# Prepare an offer

`POST /api/market/offers/prepare`

Step 1 of making an offer on a UTXO, listed or not: checks the UTXO (tracked sats, no inscription or rune, unspent and confirmed, not yours), picks plain cardinal UTXOs of your payment address and returns the offer transaction to sign: the seller's UTXO at input 0 (unsigned), your funding after it; the UTXO's sats to your ordinals address (output 0), `price` to the address that holds it (output 1), the platform fee (output 2), your change, the marker. The plan is held 10 minutes.

#### Request body

`content-type: application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `outpoint` | string | yes | The UTXO to bid on, `txid:vout`: any UTXO holding tracked sats, unspent and confirmed, with no inscription or rune, not your own. Listed or not. |
| `price` | integer | yes | What the seller receives, in sats: above the UTXO's value, at least 546. You pay it plus the platform fee and the network fee. |
| `bidder` | object | yes | The bidder's wallet: Xverse-style wallets have two addresses, UniSat-style one (use it for both). |
| `bidder.payment_address` | string | yes | Funds the offer and gets the change. Taproot, native SegWit or nested SegWit. |
| `bidder.payment_public_key` | string | yes | Its public key, hex (Taproot: the internal key, x-only or compressed; SegWit: compressed). |
| `bidder.ordinals_address` | string | yes | Receives the UTXO. Taproot, native SegWit or nested SegWit. May equal `payment_address`. |
| `bidder.ordinals_public_key` | string | yes | Its public key, hex (Taproot: the internal key, x-only or compressed; SegWit: compressed). Required: the ordinals address can cancel the offer and lists it as made. |
| `fee_rate` | number | yes | The network fee rate in sat/vB (see `GET /api/market/fees`), fixed when you sign. (max 5000) |
| `expires_in` | integer \| `"never"` \| null | no | How long it can be accepted, in seconds: 86400 (1 day), 259200 (3 days), 604800 (7 days, the default when absent) or 2592000 (30 days); `null` or `"never"` for no expiry. |

#### Example request

```bash title="cURL"
curl -X POST 'https://raresats.net/api/market/offers/prepare' \
  -H 'content-type: application/json' \
  -d '{
  "outpoint": "24b3eedfdc210f4a5baa96a6c99db5a51d3b1286ab2f58cb0b16fc607e54107f:0",
  "price": 110000,
  "bidder": {
    "payment_address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
    "payment_public_key": "0283dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145",
    "ordinals_address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
    "ordinals_public_key": "0283dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145"
  },
  "fee_rate": 3,
  "expires_in": 604800
}'
```
```js title="JavaScript"
const response = await fetch('https://raresats.net/api/market/offers/prepare', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    "outpoint": "24b3eedfdc210f4a5baa96a6c99db5a51d3b1286ab2f58cb0b16fc607e54107f:0",
    "price": 110000,
    "bidder": {
      "payment_address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
      "payment_public_key": "0283dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145",
      "ordinals_address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
      "ordinals_public_key": "0283dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145"
    },
    "fee_rate": 3,
    "expires_in": 604800
  })
});
const data = await response.json();
```
```python title="Python"
import requests

data = requests.post("https://raresats.net/api/market/offers/prepare", json={
    "outpoint": "24b3eedfdc210f4a5baa96a6c99db5a51d3b1286ab2f58cb0b16fc607e54107f:0",
    "price": 110000,
    "bidder": {
        "payment_address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
        "payment_public_key": "0283dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145",
        "ordinals_address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
        "ordinals_public_key": "0283dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145"
    },
    "fee_rate": 3,
    "expires_in": 604800
}).json()
```

#### Response `200`

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | The prepared offer's id: submit with it. |
| `psbt` | string | The whole offer transaction, base64. Input 0 is the seller's UTXO, unsigned (the seller signs it to accept). |
| `sign` | array of object | Your inputs (1 and up): sign exactly these, with your wallet's default sighash. |
| `sign[].index` | integer | The input to sign. |
| `sign[].address` | string | The address whose key signs it. |
| `sign[].sighash` | integer | The sighash type to sign with, when it is not the default: 131 (`0x83`, SIGHASH_SINGLE\|ANYONECANPAY) for listings. Absent on purchase inputs: sign with the default (SIGHASH_DEFAULT for Taproot, SIGHASH_ALL for SegWit v0). |
| `summary` | object | What the offer costs and pays. |
| `summary.outpoint` | string | The UTXO bid on. |
| `summary.value` | integer | Its value in sats. |
| `summary.price` | integer | What the seller receives. |
| `summary.platform_fee` | integer | 2.5% of `price`, at least 546 sats. |
| `summary.network_fee` | integer | The network fee in sats. |
| `summary.total` | integer | What you pay: `price + platform_fee + network_fee`. |
| `summary.vsize` | integer | Estimated virtual size. |
| `summary.fee_rate` | number | sat/vB. |
| `summary.change` | integer | Your change output in sats, 0 when none. |
| `summary.unit_price` | number | `(price - value) / count`. |
| `summary.row` | string | The UTXO's own row. |
| `summary.count` | integer | Sats of `row` in the UTXO. |
| `expires_at` | integer \| null | When the offer will expire, unix seconds; `null`: no expiry. |
| `submit_by` | integer | Submit before this (unix seconds, 10 minutes from now), or prepare again. |

```json title="Response"
{
  "id": "dbf866a1e7e209e7",
  "psbt": "cHNidP8BAP0fAQIAAAACfxBUfmD8FgvLWC+rhhI7HaW1ncmmlqpbSg8h3N/usyQAAAAAAP3///9Bihr4H2G1iZuygx3BHzwLSWfyqpoJAv/W0tpPJYLJAQAAAAAA/f///wUQJwAAAAAAACJRIKgvKZRNZbhq5rXlzHXilOrWxZORoe3F4BbjSYxn/Hu7sK0BAAAAAAAiUSCmCGnw288dxlnJzsuvgFATXqnozcSHBT8dxogJSdxoTL4KAAAAAAAAIlEgrfKcXTOb9Ymo27aSDRsOplytPApoV4riNE46JGW7F/AvJxUAAAAAACJRIKgvKZRNZbhq5rXlzHXilOrWxZORoe3F4BbjSYxn/Hu7AAAAAAAAAAAOagxyYXJlc2F0cy5uZXQAAAAAAAEBKxAnAAAAAAAAIlEgpghp8NvPHcZZyc7Lr4BQE16p6M3EhwU/HcaICUncaEwAAQErYOMWAAAAAAAiUSCoLymUTWW4aua15cx14pTq1sWTkaHtxeAW40mMZ/x7uwEXIIPf6FoxUdJRcpDaRh/igVWR72nysYos5j8BaXqLMTFFAAAAAAAA",
  "sign": [
    {
      "index": 1,
      "address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"
    }
  ],
  "summary": {
    "outpoint": "24b3eedfdc210f4a5baa96a6c99db5a51d3b1286ab2f58cb0b16fc607e54107f:0",
    "value": 10000,
    "price": 110000,
    "platform_fee": 2750,
    "network_fee": 963,
    "total": 113713,
    "vsize": 321,
    "fee_rate": 3,
    "change": 1386287,
    "unit_price": 100000,
    "row": "decasat_alpha",
    "count": 1
  },
  "expires_at": 1791571812,
  "submit_by": 1790967612
}
```

#### Errors

`{error, code}`; see [Errors](https://raresats.net/docs/errors.md) for every code.

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad_address` | An address is not a mainnet address (or not one at all). |
| 400 | `bad_bidder` | The offer's `bidder` is missing, not an object, or lacks `ordinals_public_key`. |
| 400 | `bad_expiry` | `expires_in` is not 86400, 259200, 604800 or 2592000 (seconds), `null` or `"never"`. |
| 400 | `bad_fee_rate` | `fee_rate` is not a positive number up to 5000. |
| 400 | `bad_json` | The body is not valid JSON. |
| 400 | `bad_outpoint` | Not an outpoint (`txid:vout`). |
| 400 | `bad_price` | A price is not a whole number of sats. |
| 400 | `bad_public_key` | A public key is not hex, or not a valid secp256k1 key of the right size. |
| 400 | `bad_request` | The body is not a JSON object, or the engine refused a malformed request (e.g. a purchase submitted without its signed setup PSBT). |
| 403 | `cross_origin` | A POST from a browser on another site (a foreign `Origin`, or `sec-fetch-site: cross-site`). Server-side clients send no `Origin` and are not affected. |
| 405 | `method_not_allowed` | Wrong method for the route; the `allow` header lists the right ones. |
| 409 | `pending` | The UTXO's listing is being bought, or an offer of it accepted, right now. |
| 409 | `spent` | The UTXO (or a listing's UTXO) is spent or does not exist. |
| 409 | `unconfirmed` | The UTXO is not confirmed yet. |
| 409 | `unindexed` | The ord indexer does not know the UTXO yet. |
| 413 | `too_large` | The body is over 256 KiB. |
| 415 | `not_json` | A POST without `content-type: application/json`. |
| 422 | `bad_structure` | The listing PSBT is not the expected layout (pairing, version 2, locktime 0, one seller, sequences), or a purchase would pay the fee address. |
| 422 | `dust_output` | A bought UTXO is below the dust limit of your ordinals address type: use a Taproot or native SegWit ordinals address. |
| 422 | `fee_rate_too_low` | `fee_rate` is below the current minimum (`GET /api/market/fees`). |
| 422 | `fee_too_high` | The network fee would exceed the per-transaction cap (200,000 sats): lower the fee rate or move fewer. |
| 422 | `inscribed` | The UTXO carries an inscription: only rare sats are listed; the wallet tools only send it whole. |
| 422 | `insufficient_funds` | The payment address's plain UTXOs cannot pay prices, fee, padding and network fee (or a transfer's fees). Carries `available`, `prices`, `platform_fee`. |
| 422 | `key_mismatch` | The public key does not control the address. |
| 422 | `legacy_address` | A legacy (P2PKH, `1…`) address: use Taproot or SegWit. |
| 422 | `no_row` | The UTXO holds no sat of a market row. |
| 422 | `own_utxo` | The UTXO is held by one of the bidder's own addresses. |
| 422 | `price_too_high` | The price is above 21,000,000 BTC. |
| 422 | `price_too_low` | The price is at most the UTXO's own value, below 546 sats, or below the payout output's dust limit. |
| 422 | `pubkey_mismatch` | The trade engine found the public key is not the address's key. |
| 422 | `runes` | The UTXO carries runes (the wallet tools only send it whole). |
| 422 | `unsupported_address` | An address type the marketplace does not trade (only Taproot, native SegWit and nested SegWit). |
| 422 | `unsupported_script` | The trade engine met a script type it does not support. |
| 422 | `untracked` | The UTXO holds no rare sat the index tracks (send it whole instead). |
| 429 | `rate_limited` | Over 30 POSTs a minute from your IPv4 address or IPv6 /64 (in chat, counted per IPv4 address or IPv6 /64: 30 writes and 20 login nonces a minute, 20 sign-ins and 10 display-name changes an hour; one message every 3 s and 20 a minute per address); wait `retry-after` seconds. With the AI moderation on, at most 6 messages and names a minute are checked per address and per IPv4 address or IPv6 /48. |
| 500 | `invariant_violated` | A planned or finalized purchase or transfer broke an invariant (`violations`); nothing was broadcast. |
| 503 | `bitcoind_unavailable` | bitcoind is not available. |
| 503 | `busy` | Too many purchases, offers or transfers are being prepared: try again in a minute. Each client (IPv4 address or IPv6 /64) keeps its 10 latest prepared purchases, offers and transfers; an older one answers `unknown_purchase` / `unknown_offer` / `unknown_transfer`. |
| 503 | `engine_unavailable` | The trade engine is not available. |
| 503 | `fork_behind` | The rare-sat index has not caught up with bitcoind: try again in a minute. |
| 503 | `fork_unavailable` | The rare-sat index is not available. |
| 503 | `indexer_behind` | The ord indexer is catching up: try again shortly. |
| 503 | `market_unavailable` | The marketplace is closed right now (its backends disagree on the network, or are unreachable). |
| 503 | `oracle_unavailable` | The ord oracle is not available. |

```json title="Error (422)"
{
  "error": "24b3eedfdc210f4a5baa96a6c99db5a51d3b1286ab2f58cb0b16fc607e54107f:0 is held by your own address: make offers on other people's UTXOs",
  "code": "own_utxo",
  "outpoint": "24b3eedfdc210f4a5baa96a6c99db5a51d3b1286ab2f58cb0b16fc607e54107f:0"
}
```

Then sign the PSBT, exactly the inputs in `sign`, and [submit](https://raresats.net/docs/submit-offer.md) before `submit_by`. The whole flow: [Make, accept and cancel offers](https://raresats.net/docs/guide-offers.md).

## Expiry

`expires_in` is 1, 3, 7 or 30 days in seconds (7 days when absent), or `null` / `"never"` for an offer with **no expiry**. The server stops accepting an offer once it expires and deletes your signatures. One with no expiry can be accepted at its price at any time until you cancel it, it is accepted, or it becomes invalid (the UTXO moves or your funding is spent). Its fee rate is fixed now: the seller can speed up a slow confirmation by spending their output 1 (CPFP).
