Skip to content

Offers

Choose an exact-target offer when you want specific asset outpoints. Choose a collection offer when any qualifying prepared unit should be eligible, including units prepared after you place it. Both can settle without the buyer returning to sign at acceptance.

You need your address’s session, asset-free BTC funding, and a signer that supports the returned PSBT instructions. The accepting seller pays the settlement fees; check their net proceeds in the acceptance quote.

Exact-target offers Collection offers
Protocol exact_offer_v1 funded_policy_offer_v1
Routes /v1/offers, /v1/exact-offer-authorizations /v1/policy-offers, /v1/policy-accepts
Covers Only the asset outpoints the buyer signed for Any qualifying one-unit asset UTXO, now or later, for the offer’s life
Buyer signs One SIGHASH_ALL settlement per target A zero-fee funding parent per alternative
Who finishes The named seller, alone The seller plus the market signer
Placing costs A network fee to create exact-value funding slots (unless you already have one) Nothing: the parent is not broadcast until acceptance

A funding slot (funding_slot_id = "<txid>:<vout>") pays for one purchase. Policy rows and exact targets only say what qualifies. One slot may back hundreds of alternative policies and targets, but the first valid seller signature wins and retires every sibling. For ten independent purchases, fund ten slots. Never add up targets or policies as if they were separate liquidity.

Each slot is a clean BTC output worth exactly the price (plus 330 sats for attached delivery). No fee is prefunded; the accepting seller pays it.

  • Already have one? Verify it: POST /v1/offer-funding-preflights with txid, vout. The answer confirms it is unspent, asset-free, and yours.
  • Otherwise POST /v1/offer-funding-templates with price_sats and slot_count (1..20). Sign and finalize the self-send locally, then POST /v1/funded-offers with funding_tx_hex, the slot bids, targets, and expires_at. This broadcasts a Bitcoin transaction and places the policies.

POST /v1/offers with existing bids, targets, and expires_at:

An asset policy can optionally include target_outpoint: {txid, vout}. This restricts every acceptance, counteroffer and authorization to that exact attached unit, whether listed or unlisted. Omit it for asset-wide eligibility. Up to 20 direct unit targets are allowed per placement. Public and owner offer reads expose target_txid and target_vout; these offers do not count toward generic asset or collection bid depth. The asset book returns them in direct_offers, never in offers. Different targets may reuse a funding slot, but still represent only one possible purchase.

The target is immutable. A spend, disappearance, or loss of its asset retires the offer; it never follows the asset into a new output. If an unrelated spend is later reorganized away, place the targeted offer again after chain state is verified. Recovery of the marketplace’s own pending settlement continues to use its existing signed transaction.

{
"bids": [{ "txid": "bbbb...bbbb", "vout": 0 }],
"targets": [{ "scope": "collection", "collection": "rare-pepe", "series": 1 }],
"expires_at": 1792000000
}

A target is {"scope": "asset", "asset": "..."} or a collection predicate over series, artist, issued_year, min_supply_units, and max_supply_units. Arbitrary trait name/value expressions are not part of this route. A request can carry 20 slots and 500 targets, at most 1,000 slot/target combinations.

A policy row alone is not immediately acceptable. To let named sellers finish without you, sign exact authorizations:

  1. Find targets: GET /v1/offers/:id/authorization-targets (live listings your policy matches but has not authorized) or GET /v1/collections/:slug/offer-targets. Known unlisted outpoints work too.
  2. POST /v1/offers/:id/exact-authorizations/batch with 1..100 {utxo_txid, utxo_vout} targets. The batch is validated whole and stored all or nothing. Each target’s asset UTXO needs 3 confirmations; a younger one is refused with 409 state_changed, retryable: true, and its confirmations / required_confirmations, so schedule the retry for when it is deep enough rather than polling.
  3. Sign each returned buyer PSBT: input 0 only, DEFAULT|ALL, then POST /v1/exact-offer-authorizations/:id/bidder-psbt with psbt_hex.

DEFAULT|ALL means the signature must commit to every input and output; on Taproot both SIGHASH_DEFAULT (0x00) and SIGHASH_ALL (0x01) do. When the PSBT input already declares a sighashType, sign with that value: most signing libraries refuse a different one.

Terminal window
curl -sS -X POST "$API/v1/offers/$OFFER_ID/exact-authorizations/batch" \
-H 'Content-Type: application/json' -H "x-session-token: $TOKEN" \
-d '{"targets":[{"utxo_txid":"YOUR_ASSET_TXID","utxo_vout":0}]}'

An authorization is a signed snapshot of that outpoint. A new or moved output needs a fresh authorization.

  • Find incoming offers: GET /v1/address/:address/exact-offers (seller session), or GET /v1/asset-utxos/:txid/:vout/exact-offers for one unit.
  • GET /v1/exact-offer-authorizations/:id (seller session) returns the unsigned settlement template, the input-1 signing instruction, and the economics. When fee rates have risen it also returns a one-input, one-output CPFP child that pays the bump from the seller’s proceeds.
  • Judge the sale by fee_adjustment.final_seller_proceeds_sats: what you keep after the child’s fee. The top-level seller_proceeds_sats is the parent’s seller output before any CPFP. fee_adjustment.state is ready (no child), seller_cpfp_required (sign cpfp too), or unavailable (the bump cannot be paid from proceeds right now; reason says why).
  • Sign input 1 with DEFAULT|ALL (and the CPFP child if present), then POST /v1/exact-offer-authorizations/:id/accept with psbt_hex and optional cpfp_psbt_hex. The server merges the buyer’s stored signature, journals, and broadcasts.

Status for either party: GET /v1/exact-offer-authorizations/:id/status. A seller may instead answer with a private counter (POST /v1/exact-offer-authorizations/:id/counter-preflight, then /counter); it must beat the offer by at least 1,000 sats in 1,000-sat steps.

Action Route
Cancel one policy POST /v1/offers/:id/cancel
Cancel one placement’s policies POST /v1/offer-placements/:id/cancel
Cancel one exact authorization POST /v1/exact-offer-authorizations/:id/cancel
Release the funds on-chain POST /v1/offer-slot-releases: an unsigned self-send of your slots back to you

POST /v1/offer-slot-releases takes slots (1..20 {txid, vout}), bidder_public_key (P2TR: the 32-byte x-only internal key; P2WPKH: the 33-byte compressed key; it must own the slots), and an optional fee_rate. It stores nothing. Sign signing.inputs_to_sign, finalize, check the txid against expected_txid, and broadcast the bytes yourself or relay them with POST /v1/transactions (raw_tx_hex). Once the spend is seen, every offer and authorization on those slots retires.

Marketplace cancellation is not Bitcoin revocation. A signed authorization stays valid on Bitcoin until its slot is spent. Spending the slot (the release self-send) retires every offer and authorization on it.

Collection offers (funded_policy_offer_v1)

Section titled “Collection offers (funded_policy_offer_v1)”

A collection, trait, or asset offer that any holder of a qualifying unit can fill, listed or merely prepared, without the buyer returning.

You sign a version-3, zero-fee funding parent. Its output 0 is a Taproot offer output worth exactly your price, whose script commits to the price, expiry, policy, and your delivery address. The parent also spends a small 330-sat market anchor that the market signs only at acceptance. Because it pays no fee, the parent cannot relay alone, so nothing appears on-chain until a seller accepts. The seller builds a child that spends your offer output and their asset UTXO, detaches the unit to you, and pays the package fee from their proceeds; the market signer co-signs it only while the offer is live and the unit matches the policy. Request, response, and signing schemas are in the API reference.

  1. POST /v1/policy-offers/preflight with bidder_public_key (P2TR: 32-byte x-only key; P2WPKH: 33-byte compressed key), optional delivery_address, and 1..100 alternatives of {price_sats, expires_at, policy}. It selects confirmed, asset-free coins (or verifies explicit funding), assigns an anchor, and returns one parent per alternative plus a fund_policy_offer wallet request. It reserves nothing.
  2. Sign every parent. The one top-level signing instruction (inputs_to_sign, sighash: "DEFAULT") applies to each alternative’s psbt_hex. Verify that the offer output equals your price and that the parents pay no network fee.
  3. POST /v1/policy-offers with bidder_public_key and the signed alternatives (psbt_hex, leaf_hex, policy each). All or nothing; an identical retry answers already_placed.

A policy has exactly these keys, with absent traits null:

{
"scope": "collection",
"asset": null,
"collection": "rare-pepe",
"max_supply_units": null,
"min_supply_units": null,
"issued_year": null,
"series": 1,
"artist": null
}

expires_at must be between 10 minutes and 90 days from now. All alternatives share one funding set, so at most one can fill. N units need N separate funding sets. Limits: 8 funding inputs, 100 alternatives, 25 open funding sets per address (409 open_offer_limit). 503 offer_capacity_full means offer capacity is temporarily unavailable; retry with backoff.

Books: GET /v1/assets/:asset/policy-offers, GET /v1/collections/:slug/policy-offers. Yours: GET /v1/address/:address/policy-offers.

  • Soft cancel, free and instant: sign the BIP-322 message digirare policy-offer cancel <parent_txid> and POST /v1/policy-offers/:id/cancel with address and signature. The market signer refuses that parent from then on. Your coins stay where they are.
  • Hard cancel, trustless: POST /v1/policy-offers/:id/cancel-transaction (optional fee_rate) returns an unsigned self-send of every funding input. Sign signing.inputs_to_sign, finalize, and broadcast it yourself or relay it with POST /v1/transactions. It kills every alternative of that funding set on-chain. Once the spend is seen, those alternatives read invalid in GET /v1/address/:address/policy-offers, even if you soft-cancelled first.

Expiry is enforced by the market signer’s clock, not by Bitcoin script: an expired parent is still valid Bitcoin until its funding or anchor is spent. Its coins are free again the moment it expires, as after a soft cancel: new offers, attaches, and purchases may spend them, and spending them kills the old parent.

  1. POST /v1/policy-accepts/preflight with parent_txid, your one-unit asset_txid/asset_vout, and optionally fee_rate and seller_min_net_sats. The quote shows seller_proceeds_sats, platform_fee_sats, and network_fee_sats (the whole package). Without fee_rate the package pays 20% above the next-block rate: a zero-fee parent and its child travel as a package, which pools were slower to mine at the plain rate.
  2. Sign input 1 of the child and POST /v1/policy-accepts with accept_id and psbt_hex.
  3. 200 means done; 202 means the journaled package is still relaying. Poll GET /v1/policy-accepts/:id and never sign a new child while it is signed, journaled, or broadcast.

The unit must be attached, confirmed, and indexed before it can accept. A refusal from the market signer arrives as policy_offer_refused with a typed refusal_code.

Each acceptance is its own transaction and its own signature. To sell into many offers at once, pick one offer per funding slot and one per unit, then accept them in order:

  1. For exact offers, POST /v1/offer-acceptance-selections/validate with up to 20 offer_ids first. It reserves nothing; it refuses a selection whose offers share a funding slot (409 duplicate_funding_slot, with the conflicts) or can no longer execute (offer_selection_unavailable). Collection offers carry funding_slot_id too: never pick two that share one.
  2. Accept each one as above. Re-quote a collection offer whose quote expired and sign it only if it still pays what you checked.
  3. Stop at the first refusal. Earlier acceptances stand; build a new selection for the rest.

Every response carries an unsigned template, your own signature, or a broadcast transaction. The server merges the other party’s signature at completion and never serves it to you.