Prepare a purchase
POST/api/market/purchase/prepare
Step 1 of buying 1-20 listings: checks they are active and unspent, picks plain cardinal UTXOs of your payment address, and returns the PSBTs to sign (a setup PSBT first when you lack padding), the fee breakdown and an expiry. The plan is held 10 minutes.
Request body #
content-type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
listing_ids | array of string | yes | 1 to 20 (max_bulk) active listings, each once. (1-20 items) |
buyer | object | yes | The buyer's wallet: Xverse-style wallets have two addresses, UniSat-style one (use it for both). |
buyer.payment_address | string | yes | Pays: padding, funding and change. Taproot, native SegWit or nested SegWit. |
buyer.payment_public_key | string | yes | Its public key, hex (Taproot: the internal key, x-only or compressed; SegWit: compressed). |
buyer.ordinals_address | string | yes | Receives the bought UTXOs. Taproot, native SegWit or nested SegWit. May equal payment_address. |
buyer.ordinals_public_key | string | null | no | Its public key, hex. Optional; checked when given. |
fee_rate | number | yes | Network fee rate in sat/vB: at least minimum from GET /api/market/fees (and at least 1), at most 5000. (max 5000) |
affiliate | string | null | no | Optional: the referring affiliate's address. It wins over the rs_ref cookie the site sets from a referral link. Any address of the network is an affiliate, with no registration; an unusable one (another network's, or the fee address) is ignored (invalid), never refused. |
Example request #
curl -X POST 'https://raresats.net/api/market/purchase/prepare' \
-H 'content-type: application/json' \
-d '{
"listing_ids": ["1031931af1592e1c"],
"buyer": {
"payment_address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
"payment_public_key": "039b3b694b8fc5b5e07fb069c783cac754f5d38c3e08bed1960e31fdb1dda35c24",
"ordinals_address": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"ordinals_public_key": "03cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115"
},
"fee_rate": 4,
"affiliate": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"
}'const response = await fetch('https://raresats.net/api/market/purchase/prepare', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
"listing_ids": ["1031931af1592e1c"],
"buyer": {
"payment_address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
"payment_public_key": "039b3b694b8fc5b5e07fb069c783cac754f5d38c3e08bed1960e31fdb1dda35c24",
"ordinals_address": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"ordinals_public_key": "03cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115"
},
"fee_rate": 4,
"affiliate": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"
})
});
const data = await response.json();import requests
data = requests.post("https://raresats.net/api/market/purchase/prepare", json={
"listing_ids": ["1031931af1592e1c"],
"buyer": {
"payment_address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
"payment_public_key": "039b3b694b8fc5b5e07fb069c783cac754f5d38c3e08bed1960e31fdb1dda35c24",
"ordinals_address": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"ordinals_public_key": "03cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115"
},
"fee_rate": 4,
"affiliate": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"
}).json()Response 200 #
| Field | Type | Description |
|---|---|---|
id | string | The prepared purchase: submit it with this id. |
setup | object | Present only when your payment address lacks padding: a setup transaction that makes it, to sign and submit too (broadcast first). |
setup.psbt | string | The unsigned PSBT, base64. The sellers' inputs carry no signature (the server adds them at submit). |
setup.sign | array of object | Your inputs: sign exactly these, with your wallet's default sighash. |
setup.sign[].index | integer | The input to sign. |
setup.sign[].address | string | The address whose key signs it. |
setup.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). |
purchase | object | The purchase transaction to sign. |
purchase.psbt | string | The unsigned PSBT, base64. The sellers' inputs carry no signature (the server adds them at submit). |
purchase.sign | array of object | Your inputs: sign exactly these, with your wallet's default sighash. |
purchase.sign[].index | integer | The input to sign. |
purchase.sign[].address | string | The address whose key signs it. |
purchase.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 | The fee breakdown and what you buy. |
summary.prices | integer | Sum of the listing prices. |
summary.platform_fee | integer | The platform fee the buyer pays: max(ceil(prices × fee_bps / 10000), min_fee) (2.5%, at least 546 sats), the same with or without a referrer. Split between platform_share and affiliate_fee. |
summary.platform_share | integer | The part of platform_fee paid to the fee address (all of it without an applied referral). |
summary.affiliate_fee | integer | The part paid to the affiliate, in its own output right after the platform's (0 unless affiliate_status is applied). |
summary.affiliate_address | string | null | The referrer (normalized), or null when none was given or it was not a usable address. |
summary.affiliate_status | string | none (no referrer), invalid (not a usable address of the network, or the fee address), ignored_self (a self-referral; see affiliate_reason), applied (paid), below_minimum (a share would be under 546 sats: the platform keeps the fee). |
summary.affiliate_reason | string | null | Why an ignored_self referral was not paid: same_address (the referrer is the buyer's payment or ordinals address), affiliate_wallet (the buyer's address belongs to the wallet that registered the affiliate, when it used the optional registration), linked_funds (the buyer's coins came from the affiliate's addresses, or the affiliate's from the buyer's), unchecked (the funds could not be checked in time: no split), seller (the referrer is a seller's address or payout). null otherwise. |
summary.affiliate_share_bps | integer | The affiliate's share of the fee in force, basis points. |
summary.network_fee | integer | Network fee of the purchase plus the setup transaction. |
summary.purchase_network_fee | integer | Network fee of the purchase transaction. |
summary.setup_network_fee | integer | Network fee of the setup transaction (0 without one). |
summary.total | integer | What the purchase costs you: prices + platform_fee + network_fee. The padding you spend comes back to you. |
summary.vsize | integer | Virtual size of the purchase transaction, vB. |
summary.setup_vsize | integer | Virtual size of the setup transaction, vB (0 without one). |
summary.fee_rate | number | The fee rate asked for, sat/vB. |
summary.change | integer | Change back to your payment address (0: none; a remainder below dust goes to the network fee). |
summary.padding_value | integer | Value of each new padding output (600). |
summary.items | array of object | The listings bought, in purchase order. |
summary.items[].id | string | The listing. |
summary.items[].outpoint | string | Its UTXO. |
summary.items[].value | integer | The UTXO's value (it lands whole in your ordinals address). |
summary.items[].price | integer | The listing price. |
summary.items[].unit_price | number | The listing's unit price. |
summary.items[].row | string | Its own row. |
summary.items[].count | integer | Sats of that row. |
expires_at | integer | Unix seconds: the plan is held 10 minutes. Submit before then, or prepare again. |
{
"id": "5c9e5c410688cd85",
"purchase": {
"psbt": "cHNidP8BAP3lAQIAAAAEV/MhdPD/0sIHo8hX35p9GQud7geCW9jXa3hoAejEjgoAAAAAAP3///+3Sw6TNUSithUu7/zVH0UQDemfGc8HWcjxkuB/9nd1LAAAAAAA/f///6nTU92uYg6gtb+FEVnT5JxNFJ1H13OjJCln4+xg+7YvAAAAAAD9////VHDf42dMx3y1IrYZcBnC/YiD1iNsIqK5yRDtpRMrGUYAAAAAAP3///8JsAQAAAAAAAAXqRQ/tulYEuV7tGkfmkpiiGKmGk92m4ciAgAAAAAAACJRIKYIafDbzx3GWcnOy6+AUBNeqejNxIcFPx3GiAlJ3GhMguwAAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4vUCAAAAAAAAIlEgrfKcXTOb9Ymo27aSDRsOplytPApoV4riNE46JGW7F/D1AgAAAAAAACJRIKgvKZRNZbhq5rXlzHXilOrWxZORoe3F4BbjSYxn/Hu7WAIAAAAAAAAXqRQ/tulYEuV7tGkfmkpiiGKmGk92m4dYAgAAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abh3hIAQAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHAAAAAAAAAAAOagxyYXJlc2F0cy5uZXQAAAAAAAEBIFgCAAAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHAQQWABT5kGeayv4lwnYVNztAvyJEbST/RAABASBYAgAAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abhwEEFgAU+ZBnmsr+JcJ2FTc7QL8iRG0k/0QAAQEfIgIAAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4gABASDwSQIAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abhwEEFgAU+ZBnmsr+JcJ2FTc7QL8iRG0k/0QAAAAAAAAAAAAA",
"sign": [
{"index": 0, "address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf"},
{"index": 1, "address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf"},
{"index": 3, "address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf"}
]
},
"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": "2fb6fb60ece3672924a373d7479d144d9ce4d3591185bfb5a00e62aedd53d3a9:0",
"value": 546,
"price": 60546,
"unit_price": 60000,
"row": "black_rare",
"count": 1
}
]
},
"expires_at": 1790953585
}Errors #
{error, code}; see Errors for every code.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_address | An address is not a mainnet address (or not one at all). |
| 400 | bad_buyer | buyer is not an object. |
| 400 | bad_fee_rate | fee_rate is not a positive number up to 5000. |
| 400 | bad_ids | ids / listing_ids is empty, too long, malformed, or lists an id twice. |
| 400 | bad_json | The body is not valid JSON. |
| 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. |
| 404 | not_found | No such listing or offer. In chat: no such message. A display name to remove that does not exist. |
| 405 | method_not_allowed | Wrong method for the route; the allow header lists the right ones. |
| 409 | not_active | A listing (or offer) is not active: being bought (or accepted), sold, cancelled, moved, expired or invalid. Carries id and status. |
| 409 | spent | The UTXO (or a listing's UTXO) is spent or does not exist. |
| 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 | 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 | pubkey_mismatch | The trade engine found the public key is not the address's key. |
| 422 | too_many_items | Too many UTXOs in a listing, or listings in a purchase (max_bulk); for a wallet transfer, too many rare sats (and the runs between them) to lay out in one UTXO: choose fewer. |
| 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. |
| 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. |
| 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_unavailable | The rare-sat index is not available. |
| 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. |
{
"error": "listing 1031931af1592e1c is pending",
"code": "not_active",
"id": "1031931af1592e1c",
"status": "pending"
}Then sign the setup PSBT (when present) and the purchase PSBT, exactly the inputs in their sign lists, and submit before expires_at. The whole flow: Buy listings.
Affiliates #
affiliate names the referrer of the purchase: any address of the network but the fee address, with no registration; it is never paid for a self-referral. Without it, the server uses the rs_ref cookie the site sets when a buyer follows a referral link (?ref=<address> or /r/<address>); the body's field wins. The summary says what happened (affiliate_status) and how the platform fee is split (platform_share, affiliate_fee). A referrer never makes prepare fail. See Affiliates.