# Buy listings

Buying is three steps: **prepare** (the server plans the purchase), **sign** your inputs, **submit** (the server adds the sellers' signatures and broadcasts). One purchase buys 1 to 20 listings.

## Before you start

- Your wallet needs a **payment address** (pays) and an **ordinals address** (receives the bought UTXOs). Xverse-style wallets have both; a single-address wallet (UniSat-style) uses one address for both. Taproot, native SegWit or nested SegWit; not legacy.
- Only **plain cardinal** UTXOs of the payment address are spent: confirmed, no inscription or runes, and no rare sat the index tracks. Your rare sats are never used as padding or funding.
- Pick a fee rate from `GET /api/market/fees` (`slow`, `normal`, `fast`; at least `minimum`).

## 1. Prepare

```json title="Request"
{
  "listing_ids": ["1031931af1592e1c"],
  "buyer": {
    "payment_address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
    "payment_public_key": "039b3b694b8fc5b5e07fb069c783cac754f5d38c3e08bed1960e31fdb1dda35c24",
    "ordinals_address": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
    "ordinals_public_key": "03cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115"
  },
  "fee_rate": 4,
  "affiliate": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"
}
```

`affiliate` is optional: the affiliate (any address) that referred you (it wins over the site's `rs_ref` referral cookie). The answer:

```json title="Response"
{
  "id": "5c9e5c410688cd85",
  "purchase": { "psbt": "cHNidP8BAP3lAQIAAAAE…", "sign": [{ "index": 0, "address": "37Vuc…Shgf" }, { "index": 1, "address": "37Vuc…Shgf" }, { "index": 3, "address": "37Vuc…Shgf" }] },
  "summary": {
    "prices": 60546, "platform_fee": 1514, "platform_share": 757, "affiliate_fee": 757,
    "affiliate_address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh", "affiliate_status": "applied", "affiliate_reason": null,
    "affiliate_share_bps": 5000,
    "network_fee": 2652, "purchase_network_fee": 2652, "setup_network_fee": 0,
    "total": 64712, "vsize": 663, "setup_vsize": 0, "fee_rate": 4, "change": 84088, "padding_value": 600,
    "items": [{ "id": "1031931af1592e1c", "outpoint": "2fb6…d3a9:0", "value": 546, "price": 60546, "unit_price": 60000, "row": "black_rare", "count": 1 }]
  },
  "expires_at": 1790953585
}
```

### The fee breakdown

- `prices`: the listings' prices, paid to the sellers.
- `platform_fee`: `max(ceil(prices × 2.5%), 546)`, one output to the fee address. With an applied referral it is split in two outputs, `platform_share` and `affiliate_fee` (see [Affiliates](https://raresats.net/docs/guide-affiliates.md)); you pay the same either way.
- `network_fee`: the purchase's network fee plus the setup's, if any.
- `total` = `prices + platform_fee + network_fee`: what the purchase costs you. The padding you spend comes back to you as new padding outputs; `change` returns to your payment address.

### Padding and the setup transaction

The purchase follows the standard layout (see [Concepts](https://raresats.net/docs/concepts.md#the-transaction-layout)): it spends k+1 small **padding** UTXOs of yours first, so that the sats of each listed UTXO land whole, at offset 0, in their own output to your ordinals address; and it makes k+1 new 600-sat padding outputs for your next purchase.

If your payment address does not have k+1 padding UTXOs plus funding, the answer also has a **`setup`** PSBT: a transaction that makes the padding first (no platform fee, no marker), which the purchase then spends. Sign both; the server broadcasts the setup first. `summary.setup_network_fee` and `setup_vsize` are its cost. After one purchase you have padding for the next.

### Expiry

The prepared plan is held for 10 minutes (`expires_at`, unix seconds). Submit before then. Afterwards submit answers 409 `expired`: prepare again (the listings may have sold meanwhile).

## 2. Sign

Sign **exactly** the inputs in `purchase.sign` (and `setup.sign`) with your wallet's default sighash: SIGHASH_DEFAULT for Taproot, SIGHASH_ALL for SegWit v0. The sellers' inputs carry no signature yet; leave them. Don't broadcast. Finalized or not is fine.

- **UniSat:** `unisat.signPsbts([setupHex, purchaseHex], [{autoFinalized: false, toSignInputs}, …])` signs both in one prompt.
- **Xverse:** `signPsbt` once per PSBT with `signInputs: {[payment address]: [indexes]}` and `broadcast: false`.

## 3. Submit

```json title="Request"
{ "id": "5c9e5c410688cd85", "setup_psbt": "<signed setup, when there is one>", "purchase_psbt": "<signed purchase>" }
```

The server checks that the signed transactions are exactly the prepared ones, verifies your signatures, adds the sellers', rechecks every rule (the sats' landing places, the pairing, the fee, the marker, the network fee), runs `testmempoolaccept` on both, and broadcasts: setup first, then the purchase.

```json title="Response"
{ "txid": "6897451c42f02de2594abfdd6a5d4fcf4794a62cbbd9fbec1ec983893ddc5df4" }
```

The listings are now `pending` with that `txid`, and `sold` once it confirms. The UTXOs arrive at your ordinals address.

### When something goes wrong

| Code | Status | What to do |
| --- | --- | --- |
| `not_active`, `spent` | 409 | A listing sold or moved meanwhile. Drop it and prepare again with the rest. |
| `conflict` | 409 | A lost race: a listing (or one of your UTXOs) was spent between prepare and broadcast. Prepare again. |
| `expired` | 409 | Over 10 minutes: prepare again. |
| `config_changed` | 409 | The marketplace's fee settings changed since prepare: prepare again and review the new summary. |
| `psbt_mismatch` | 422 | The signed PSBT is not the prepared transaction (a wallet changed it): sign the PSBT as given. |
| `insufficient_funds` | 422 | Not enough plain funds at the payment address; the error says how much is `available`. |
| `fee_rate_too_low` | 422 | Use at least `minimum` from `/api/market/fees`. |
| `dust_output` | 422 | A bought UTXO is too small for your ordinals address type: use a Taproot or native SegWit ordinals address. |
| `broadcast_unknown` | 503 | The broadcast's outcome is not known yet. **Submit the same body again**: resubmitting is idempotent, and answers `{txid}` once bitcoind has it. |

A lost answer (a timeout, a dropped connection) is handled the same way as `broadcast_unknown`: submit again. Your listings stay reserved for you meanwhile, and return to sale if the transaction never reaches the mempool.

## A complete Node example

A script for a single-address Taproot wallet (it pays and receives with one key): Node 18+, `npm install bitcoinjs-lib tiny-secp256k1 ecpair`.

```js title="buy.mjs"
// node buy.mjs <listing id> [<listing id> …]
// WIF: the private key (mainnet WIF) of the Taproot address that pays and receives.
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import { ECPairFactory } from 'ecpair';

bitcoin.initEccLib(ecc);
const ECPair = ECPairFactory(ecc);
const BASE = process.env.RARESATS_URL ?? 'https://raresats.net';
const ids = process.argv.slice(2);

const key = ECPair.fromWIF(process.env.WIF);
const internalKey = key.publicKey.subarray(1, 33);
const { address } = bitcoin.payments.p2tr({ internalPubkey: internalKey });
const publicKey = Buffer.from(key.publicKey).toString('hex');
const signer = key.tweak(bitcoin.crypto.taggedHash('TapTweak', internalKey));

async function call(path, body) {
  const response = await fetch(`${BASE}${path}`, body === undefined ? {} : {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body)
  });
  const json = await response.json();
  if (!response.ok) {
    throw Object.assign(new Error(`${path}: ${response.status} ${json.code}: ${json.error}`), { code: json.code });
  }
  return json;
}

// 1. Prepare at the "normal" fee rate.
const fees = await call('/api/market/fees');
const prepared = await call('/api/market/purchase/prepare', {
  listing_ids: ids,
  buyer: { payment_address: address, payment_public_key: publicKey, ordinals_address: address, ordinals_public_key: publicKey },
  fee_rate: fees.normal
});
const { summary } = prepared;
console.log(`prices ${summary.prices} + platform fee ${summary.platform_fee} + network fee ${summary.network_fee} = ${summary.total} sats`);

// 2. Sign your inputs only (default sighash), the setup too when there is one.
const sign = ({ psbt, sign: inputs }) => {
  const parsed = bitcoin.Psbt.fromBase64(psbt);
  for (const { index } of inputs) {
    parsed.signInput(index, signer);
  }
  return parsed.toBase64();
};
const body = { id: prepared.id, purchase_psbt: sign(prepared.purchase) };
if (prepared.setup) {
  body.setup_psbt = sign(prepared.setup);
}

// 3. Submit. Resubmitting is idempotent: retry while the outcome is unknown.
for (let attempt = 1; ; attempt++) {
  try {
    const result = await call('/api/market/purchase/submit', body);
    console.log('broadcast', result.setup_txid ? `setup ${result.setup_txid}, purchase ${result.txid}` : result.txid);
    break;
  } catch (error) {
    if (error.code !== 'broadcast_unknown' || attempt === 5) {
      throw error;
    }
    await new Promise(resolve => setTimeout(resolve, 10_000));
  }
}
```

Every code: [Errors](https://raresats.net/docs/errors.md).
