# Account Page

Canonical: https://docs.projectbabbage.com/docs/quickstarts/metamarket/account

Fetch balances, withdraw earnings, and remove spent upload tokens on-chain.

This page shows how to add "Account" functionality so that an uploader can:

- View and withdraw their accumulated satoshis balance
- See a list of all files they’ve uploaded, both active and expired
- Remove and spend any of their uploaded file tokens

---

## 1. Backend: Uploader Queries

### 1.1. Add `findByUploaderPublicKey` and `findByUploaderPublicKeyExpired`

In `backend/src/lookup-services/MarketStorage.ts`, add methods that filter records by `publicKey` and by whether `retentionPeriod` is before or after now:

```ts
// backend/src/lookup-services/MarketStorage.ts

// … existing methods …

/**
 * Returns all active (not yet expired) files uploaded by a given publicKey.
 */
async findByUploaderPublicKey(
  publicKey: string
): Promise<UTXOReference[]> {
  return await this.records
    .find({
      creatorPublicKey: publicKey,
      retentionPeriod: { $gte: Math.floor(Date.now()) }
    })
    .project<UTXOReference>({ txid: 1, outputIndex: 1 })
    .toArray()
    .then(results =>
      results.map(rec => ({
        txid: rec.txid,
        outputIndex: rec.outputIndex
      }))
    )
}

/**
 * Returns all expired files uploaded by a given publicKey.
 */
async findByUploaderPublicKeyExpired(
  publicKey: string
): Promise<UTXOReference[]> {
  return await this.records
    .find({
      creatorPublicKey: publicKey,
      retentionPeriod: { $lt: Math.floor(Date.now()) }
    })
    .project<UTXOReference>({ txid: 1, outputIndex: 1 })
    .toArray()
    .then(results =>
      results.map(rec => ({
      txid: rec.txid,
      outputIndex: rec.outputIndex
      }))
    )
}
```

**What’s happening:**

- **`findByUploaderPublicKey`** uses the `{ $gte: now }` comparison to return only unexpired UTXO references.
- **`findByUploaderPublicKeyExpired`** uses `{ $lt: now }` to return only expired ones.
- Both return `UTXOReference[]` so that the lookup service can format responses as BEEF.

---

### 1.2. Add uploader‐specific queries to MarketLookupService

Now extend the `lookup` method to work with two new query types. Add these helpers with the others after the class:

```ts
// backend/src/lookup-services/MarketLookupServiceFactory.ts

function isUploaderFilesQuery(query: any): query is {
  type: 'findUploaderFiles'; value: { publicKey: string }
} {
  return (
    typeof query === 'object' &&
    query.type === 'findUploaderFiles' &&
    query.value &&
    typeof query.value.publicKey === 'string'
  )
}

function isUploaderFilesQueryExpired(query: any): query is {
  type: 'findUploaderFilesExpired'; value: { publicKey: string }
} {
  return (
    typeof query === 'object' &&
    query.type === 'findUploaderFilesExpired' &&
    query.value &&
    typeof query.value.publicKey === 'string'
  )
}
```

And insert these checks alongside the existing queries:

```ts
async lookup(question: LookupQuestion): Promise<LookupAnswer | LookupFormula> {
  // … other lookup cases …

  // uploader → return all active uploads for this publicKey
  if (isUploaderFilesQuery(query)) {
    const { publicKey } = query.value
    return await this.storage.findByUploaderPublicKey(publicKey)
  }

  // uploaderExpired → return all expired uploads for this publicKey
  if (isUploaderFilesQueryExpired(query)) {
    const { publicKey } = query.value
    return await this.storage.findByUploaderPublicKeyExpired(publicKey)
  }

  throw new Error('Unknown query type')
}
```

---

### 1.3. Implement `outputSpent`

The `outputSpent` method is called when a transaction that is being signed and redeemed is being broadcasted, making the UTXO on the lookup service obsolete and to be removed:

```ts
async outputSpent(payload: OutputSpent): Promise<void> {
  if (payload.mode !== 'none') throw new Error('Invalid mode')
  const { topic, txid, outputIndex } = payload
  if (topic !== 'tm_demo_market') throw new Error(`Invalid topic "${topic}" for this service.`)
  await this.storage.deleteRecord(txid, outputIndex)
}
```

Note that there is also the `outputEvicted` method, which can be invoked by the administrator of the Overlay via the LARS and CARS CLI menu. This method is used to remove outputs stored on the overlay that are in breach of the terms and conditions or are violations of the law.

---

## 2. Key Server: Balance & Withdraw

We’ll add two new routes:

1. **`POST /balance`** – return the authenticated user’s current balance
2. **`POST /withdraw`** – creates and returns a transaction, spending the satoshis stored on the key server and allowing the frontend to reallocate those satoshis to the withdrawer's wallet

### 2.1. Extend `KeyStorage` with `getBalance` and `setBalance`

In **key-server/src/KeyStorage.ts**, add these two methods:

```ts
// key-server/src/KeyStorage.ts

export class KeyStorage {
  private readonly balances: Collection<BalanceRecord>
  // … existing constructor, records collection, etc …

  /**
   * Return the caller’s current balance.
   */
  async getBalance(publicKey: string): Promise<number> {
    const res = await this.balances.findOne({ publicKey })
    return res?.balance || 0
  }

  /**
   * Sets the publicKey's balance to a new value.
   */
  async setBalance(publicKey: string, newBalance: number): Promise<void> {
    await this.balances.updateOne(
      { publicKey },
      { $set: { balance: newBalance } },
      { upsert: true }
    )
  }
}
```

**What’s happening:**

-   **`getBalance`** reads the user's current satoshi total, defaulting to `0` if there is no existing entry.
-   **`setBalance`** sets the user's satoshi count to match the newBalance.

---

### 2.2. Expose `/balance` and `/withdraw`

Next, register two new Express endpoints. Both expect a `publicKey` in the JSON body, which must exactly match the authenticated `identityKey` injected by `AuthMiddleware`.

```ts
// key-server/src/index.ts

/**
 * POST /balance
 * Returns the caller’s current balance.
 */
app.post('/balance', async (req: Request, res: Response): Promise<any> => {
  try {
    const { publicKey } = req.body
    if (!publicKey  ||  publicKey  !== (req  as  any).auth?.identityKey)
      return res.status(400).json({ error: 'Missing publicKey in body' })

    // retrieve balance from storage
    const balance = await keyStorage.getBalance(publicKey)
    console.log('User Balance:', balance)
    return res.status(200).json({
      success: true,
      balance
    })
  } catch (error) {
    console.error('Error retrieving balance:', error)
  }
})
```

```ts
/**
 * POST /withdraw
 * send satoshis stored on the server to the uploader’s own wallet.
 */
app.post('/withdraw', async (req, res): Promise<any> => {
  try {
    const { publicKey } = req.body
    if (!publicKey || publicKey !== (req as any).auth?.identityKey)
      return res.status(400).json({ error: 'Missing publicKey in body' })

    // get the public identity key of the server
    const senderIdentityKey = PrivateKey.fromHex(SERVER_PRIVATE_KEY).toPublicKey().toString()

    const derivationPrefix = randomBytes(10).toString('base64');
    const derivationSuffix = randomBytes(10).toString('base64');

    const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
      protocolID: [2, '3241645161d8'],
      keyID: `${derivationPrefix} ${derivationSuffix}`,
      counterparty: publicKey
    })

    // get the stored balance of the withdrawer
    const amount = await keyStorage.getBalance(publicKey)
    if (amount <= 0) {
      return res.status(400).json({ error: 'No balance to withdraw' })
    }

    const lockingScript = new P2PKH().lock(
      PublicKey.fromString(derivedPublicKey)
        .toAddress())
      .toHex()

    // create an on-chain payment to the withdrawer's own identityKey
    const { tx } = await wallet.createAction({
      description: `Market withdrawal from user: ${wallet.getPublicKey.toString()}`,
      outputs: [{
        satoshis: amount,
        lockingScript,
        customInstructions: JSON.stringify({
          derivationPrefix,
          derivationSuffix,
          payee: senderIdentityKey
        }),
        outputDescription: 'Market account withdraw'
      }],
      options: {
        randomizeOutputs: false
      }
    })
    if (!tx) {
      throw new Error('Error creating action')
    }

    // set the withdrawer's balance to 0
    await keyStorage.setBalance(publicKey, 0)

    // return the information required for the frontend to redeem the token
    return res.status(200).json({
      message: 'Withdraw partial tx created',
      transaction: Utils.toArray(tx, 'base64'),
      derivationPrefix,
      derivationSuffix,
      amount,
      senderIdentityKey
    })
  } catch (error) {
    console.error('Error withdrawing balance:', error)
  }
})
```

**What’s happening:**
1.  Both routes read `req.auth.identityKey`, injected by `AuthMiddleware`.
2.  `/balance` simply returns the current total.
3.  `/withdraw` resets the balance on-server and uses your server’s funded wallet to send those satoshis back to the uploader’s own BSV identity key for them to redeem.

----------

## 3. Frontend: Account Page Hooks

On `frontend/src/pages/Account.tsx`, you only need to wire up three functions:

1.  **`fetchData`** – call `/balance`, `findByUploaderPublicKey`, and `findByUploaderPublicKeyExpired`
2.  **`handleWithdraw`** – POST `/withdraw` and update local state
3.  **`handleRemoveFile`** – spend the token on-chain and broadcast that spend

```ts
// frontend/src/pages/Account.tsx
// … inside your component, after setting up state, wallet, authFetch, lookupResolver …

// 1. Fetch balance & file lists
const fetchData = async () => {
  setLoading(true)
  try {
    // get the balance from the key server
    let publicKey = (await wallet.getPublicKey({ identityKey: true }))
    const response = await authFetch.fetch(`${constants.keyServer}/balance`, {
      method: 'POST',
      body: JSON.stringify(publicKey),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    if (!response) {
      throw new Error('Error fetching account balance')
    }
    const { balance } = await response.json()

    setBalance(balance || 0)

    // get the unexpired files from the overlay
    const filesResponse = await lookupResolver.query({
      service: 'ls_demo_market',
      query: {
        type: 'findUploaderFiles',
        value: {
          publicKey: publicKey.publicKey.toString()
        }
      }
    })
    if (filesResponse.type !== 'output-list') 
      throw new Error('Lookup answer must be a output-list')

    // get the expired files from the overlay
    const expiredResponse = await lookupResolver.query({
      service: 'ls_demo_market',
      query: {
        type: 'findUploaderFilesExpired',
        value: {
          publicKey: publicKey.publicKey.toString()
        }
      }
    })
    if (expiredResponse.type !== 'output-list')
      throw new Error('Lookup answer must be a output-list')

    // decode the overlay outputs
    const filesData = await decodeOutputs(filesResponse.outputs, fields)
    const expiredData = await decodeOutputs(expiredResponse.outputs, expiredFields)
    
    setFiles(filesData as UploadedFile[])
    setExpiredFiles(expiredData as ExpiredFile[])
  } catch (err) {
    console.error('Error fetching account data:', err)
  } finally {
    setLoading(false)
  }
}
```

```ts
// 2. Withdraw earnings
const handleWithdraw = async () => {
  setLoading(true)
  try {
    // hit the /withdraw endpoint to facilitate the transaction
    const publicKey = await wallet.getPublicKey({ identityKey: true })
    const response = await authFetch.fetch(`${constants.keyServer}/withdraw`, {
      method: 'POST',
      body: JSON.stringify(publicKey),
      headers: {
        'Content-Type': 'application/json'
      }
    })
    if (!response.ok) {
      console.error('Error parsing balance result')
      return
    }
  
    // redeem the transaction and 
    const paymentData = await response.json() as WithdrawResponse
    const processedTx = await wallet.internalizeAction({
      tx: Utils.toArray(paymentData.transaction, 'base64') as AtomicBEEF,
      outputs: [{
        paymentRemittance: {
          derivationPrefix: paymentData.derivationPrefix,
          derivationSuffix: paymentData.derivationSuffix,
          senderIdentityKey: paymentData.senderIdentityKey
        },
        outputIndex: 0,
        protocol: 'wallet payment'
      }],
      description: `Withdraw from Marketplace`
    })

    console.log('Withdraw successful!', processedTx)

    setBalance(0)
  } catch (err) {
    console.error('Error when withdrawing:', err)
  } finally {
    setLoading(false)
  }
}
```

```ts
// 3. Remove (spend) a file token
const handleRemoveFile = async (txid: string, outputIndex: number) => {
  setLoading(true)
  try {
    // check that the requested file is stored on the overlay and on-chain
    const response = await lookupResolver.query({
      service: 'ls_demo_market',
      query: {
        type: 'findDetails',
        value: {
          txid,
          outputIndex: outputIndex
        }
      }
    })
    if (response.type !== 'output-list') throw new Error('Lookup answer must be an output-list')
    if (response.outputs.length != 1) throw new Error('Must be a single existing UTXO')

    // process the inputs of the transaction to be redeemed
    const inputBEEF = response.outputs[0].beef
    const outpoint = `${txid}.${outputIndex}`

    const { signableTransaction } = await wallet.createAction({
      inputBEEF,
      inputs: [{
        outpoint,
        unlockingScriptLength: 74, // PushDrop scripts tend to be around 74
        inputDescription: 'Marketplace upload token',
      }],
      description: 'Spending token from marketplace'
    })
    if (!signableTransaction) throw new Error('Token must be spendable')

    // sign the action and redeem the tokens that were stored on the key server
    const unlocker = pushdrop.unlock([2, 'marketplace'], '1', 'anyone', undefined, true)
    const partialTx = Transaction.fromAtomicBEEF(signableTransaction.tx)
    const unlockingScript = await unlocker.sign(partialTx, 0)

    const action = await wallet.signAction({
      reference: signableTransaction.reference,
      spends: {
        0: {
          unlockingScript: unlockingScript.toHex()
        }
      }
    });

    console.log(action)

    if (!action) throw new Error('Spend token failed.')

    // Removing the file from the backend database
    const broadcaster = new TopicBroadcaster(['tm_demo_market'], {
      networkPreset: window.location.hostname === 'localhost' ? 'local' : 'mainnet'
    })
    const spend = await broadcaster.broadcast(Transaction.fromAtomicBEEF(action.tx as number[]))
    console.log('Token Spent:', spend)

    setFiles(prev => prev.filter(f => !(f.txid === txid && f.outputIndex === outputIndex)))
    setExpiredFiles(prev => prev.filter(f => !(f.txid === txid && f.outputIndex === outputIndex)))
  } catch (err) {
    console.error('Delete file error:', err)
  } finally {
    setLoading(false)
  }
}
```

**What’s happening:**

- **`fetchData`** loads your balance and your active and expired uploads.
- **`handleWithdraw`** calls `/withdraw` to receive the satoshis that were stored on the key server and redeems them into your own personal wallet.
- **`handleRemoveFile`** spends the PushDrop token so the lookup service removes your file and the token is no longer unspent.

---

## 4. Summary

With these additions, the **Account** page now allows an authenticated uploader to:

- **View & withdraw** their accumulated balance from users purchasing their files
- **See their listings**: both active and expired listings
- **Remove**  spend any of their file tokens, broadcasting a spend transaction so the lookup service deletes it

The directory tree should now resemble:

```text
my-marketplace/
├── backend/
│   ├── src/
│   │   ├── lookup-services/
│   │   │   ├── MarketLookupServiceFactory.ts  ← add uploader queries
│   │   │   └── MarketStorage.ts               ← add methods to facilitate the queries
│   │   ├── topic-managers/
│   │   │   └── MarketTopicManager.ts
│   │   ├── types.ts
│   │   └── … other backend files …
│   └── package.json
├── frontend/
│   ├── src/
│   │   ├── pages/
│   │   │   └── Account.tsx     ← fetchData, handleWithdraw, handleRemoveFile
│   │   ├── utils/
│   │   │   └── decodeOutputs.ts
│   │   ├── App.tsx
│   │   └── … other frontend files …
│   └── package.json
├── key-server/
│   ├── src/
│   │   ├── index.ts       ← add /balance and /withdraw handlers
│   │   ├── KeyStorage.ts  ← add getAndResetBalance, getBalance
│   │   └── types.ts
│   └── package.json
├── deployment-info.json
└── package.json
```

Now users can see their own uploads, withdraw earnings, and remove tokens, implementing a complete **Account** experience.

![Upload Files Page Success](https://docs.projectbabbage.com/assets/images/metamarket-account.webp)

Next up: we’ll cover the **Next Steps** of this project, including the steps to deploy, and optional features like search filters!
