Skip to content

Buying

Buy one or more available units by requesting a quote, signing it, and submitting the signed PSBT.

Before starting, create a session for your buying address and use an HTTP client. Your signer must support PSBT signing for the returned input indexes and sighash. Addresses, signatures, and transaction IDs shown as placeholders must be replaced with your own values.

Check the returned quote before signing: total_sats must equal subtotal_sats plus platform_fee_sats and network_fee_sats; signing.sighash must be ALL. Verify the actual PSBT against your intended purchase, sign only signing.inputs_to_sign, then submit the signed PSBT with the quote’s fill_id.

Terminal window
curl -s -X POST "$API/v1/fills/request" -H 'content-type: application/json' -d '{
"items": [{ "asset": "RAREPEPE", "quantity": 1, "max_price_sats": 120000 }],
"buyer_address": "bc1q..."
}'
curl -s -X POST "$API/v1/fills/complete" -H 'content-type: application/json' \
-H "x-session-token: $TOKEN" \
-d '{"fill_id":"...","psbt_hex":"70736274ff..."}'

A cart line is {asset, quantity, max_price_sats}, not a listing id. The API resolves each line to the cheapest listings at or under your max_price_sats when it builds the quote. max_price_sats is a per-unit ceiling and is your slippage tolerance. A cart holds at most 10 lines and 20 units.

For one particular unit, add target_outpoint: {txid, vout} to the line and set quantity: 1. The API then buys only that outpoint, even if another unit is cheaper. It never substitutes another unit. A missing, unavailable, or over-budget target returns 409 target_unavailable; a stale signed quote still returns checkout_changed. Refreshing keeps the same target and price ceiling. There is still at most one line per asset.

The quote lists exactly what you are buying in settlement_items (listing id, seller, outpoint, price) and which coins fund it in selected_outpoints.

A quote is a short-lived PSBT snapshot (about three minutes). Other buyers can hold overlapping quotes at the same time. Only POST /v1/fills/complete, after it verifies your signature byte-for-byte against the stored template, claims the listings. The first valid signature returned wins, not the first quote.

POST /v1/fills/request needs no session for a public cart. Completing it requires the buyer’s session and a SIGHASH_ALL signature on the funding inputs. Sign only signing.inputs_to_sign. Your funding inputs must be free of Counterparty assets; the API proves that before quoting.

If a listing sold, was repriced or delisted, or its UTXO moved, completion answers 409 with code checkout_changed. The body is written against your original intent:

{
"error": "checkout changed — review and sign a fresh transaction",
"code": "checkout_changed",
"can_fulfill": true,
"requires_new_signature": true,
"previous_subtotal_sats": 200000,
"current_subtotal_sats": 205000,
"previous_platform_fee_sats": 5000,
"current_platform_fee_sats": 5125,
"changes": [
{ "asset": "RAREPEPE", "requested": 2, "available": 2,
"max_price_sats": 120000, "current_subtotal_sats": 205000 }
]
}

Request a new quote and sign again. Never resubmit the old PSBT, and never have your bot silently accept a smaller quantity or a different asset: decide explicitly, then sign the new template.

Other refusals worth handling: insufficient_funds (the body carries need_sats, have_sats, shortfall_sats), signature_invalid, template_mismatch, and expired.

By default the purchased balance is detached to an address: output 0 of the checkout is an encrypted Counterparty detach OP_RETURN, and one detach message delivers every unit in the cart. You receive an ordinary address balance, not an asset UTXO.

  • delivery_address sets where it goes. It can be any Counterparty address family, legacy 1... included. It defaults to buyer_address.
  • buyer_address itself is the signing address that funds the purchase, so it must be P2TR or P2WPKH.

quote.delivery echoes mode and address. Its defaulted flag is true when you omitted delivery_mode and received the detached default; it says nothing about the address, so check delivery.address against the destination you meant.

For a single acquired unit, delivery_mode: "attached" keeps the asset on a new UTXO at the buyer’s trading address. This requires exactly one acquired asset and cannot use a different delivery address. The quote exposes the retained BTC as delivery.utxo_value_sats; include it when showing the total wallet debit. It is retained value, not a fee.

completeFill returns the broadcast txid (and already_completed: true when you repeat a completion that already happened). The server journals the exact transaction before it broadcasts and rebroadcasts it if needed.

Route Use
GET /v1/fills/:id Buyer-session status of one checkout, for a lost response
GET /v1/address/:address/in-flight-purchases Your journaled, unconfirmed purchases
GET /v1/fills Public recent fills (collection, limit, cursor)

A seller can answer your exact offer with a private counter. Fill it through the same checkout with counter_id in place of items: POST /v1/fills/request {"counter_id": "...", "buyer_address": "..."}. Only the addressed buyer can see or fill it. List yours with GET /v1/address/:address/counters.

Between broadcast and confirmation, a purchase is visible in the mempool like any Bitcoin transaction.

  • Listings. The seller’s listing signature is never served; only the market builds purchases from it. Once your purchase is broadcast, though, that signature is public, and someone could build a competing purchase that pays the seller the same price with a higher fee. You would lose the race, not money: your coins are simply not spent. Checkout broadcasts at the next-block rate so the window is short.
  • Exact offers and collection offers are fully committed transactions; no third party can reuse their signatures.
  • A bidder can back out of an accepted but unconfirmed offer by double-spending their own funding with a higher fee. The sale then fails and no asset moves.