Create a collection bid

Let's walk through the steps involved in creating a collection bid via the API on Satflow.

A collection bid offers a price for any item in a collection, or for any item with a given trait. It is funded from a bidding wallet: a 1-of-2 multisig address derived from your payment key and a Satflow key, which lets Satflow complete the sale when a seller accepts.

  1. Get your bidding wallet address from GET /address/bidding-wallet.
  2. Fund it by sending bitcoin to that address.
  3. Sign the bid message with your payment address and place the bid with POST /bid/place.
  4. Check your bids with GET /address/bids.

1. Get your bidding wallet

Call GET /address/bidding-wallet with three query parameters:

  • ordinalsAddress: the address that receives the items you win.
  • paymentAddress: the address that funds the bids and receives withdrawals.
  • paymentPubkey: the compressed public key (33 bytes, hex) of paymentAddress. For a Taproot payment address, later requests (and bidderAddressPublicKey in step 3) may send the same key in its 32-byte x-only form.

The bidding wallet address is data.multiSig.address. The first request ties the wallet to these three values. A later request with a different combination returns 400 with errorCode: "BIDDING_WALLET_IDENTITY_MISMATCH". For the common cases, a payment address of another type or a different ordinals address, the message says how to fix it.

2. Fund the bidding wallet

Send bitcoin to the bidding wallet address. Both confirmed and unconfirmed funds count toward your balance.

The balance is applied per bid pool. A pool is a collection, or a trait within a collection. Your open bids in one pool, plus the maker fee, must fit within the balance, and each pool can use the full balance on its own. When a bid fills, Satflow cancels the bids that the remaining balance no longer covers.

3. Sign and place the bid

Build this message, joining the fields with colons (a trait bid adds two lines, see Trait bids below):

bidderAddress:bidderAddressPublicKey:bidderTokenReceiveAddress:price:quantity:bidExpiry:collectionSlug:timestamp
  • bidderAddress: your payment address (the paymentAddress from step 1).
  • bidderAddressPublicKey: its public key.
  • bidderTokenReceiveAddress: your ordinals address.
  • price: the price per item in sats, a multiple of 1,000 sats. For a rune bid it is the total price for quantity units (at least 10,000 sats).
  • quantity: how many items you want, a whole number of at least 1. For runes, the number of whole units, and collectionSlug is the rune name.
  • bidExpiry: when the bid expires, in milliseconds since the Unix epoch, at least 15 minutes from now.
  • collectionSlug: the collection.
  • timestamp: the current time in milliseconds since the Unix epoch. It must be less than 2 minutes old, and no more than 30 seconds ahead of Satflow's clock, when the bid arrives.

Sign the message with the key of bidderAddress, using BIP-322 or a BIP-137 ("Bitcoin Signed Message") signature, and encode the signature in base64. Signatures from the ordinals address are not accepted.

Compute timestamp and bidExpiry when you build the message, sign it, and send the same values in the body of POST /bid/place. In JavaScript:

const timestamp = Date.now();
const bidExpiry = timestamp + 24 * 60 * 60 * 1000; // at least 15 minutes ahead; one day here

const body = {
  price: 50000,
  collectionSlug: '<collection slug>',
  bidderAddress: '<payment address>',
  bidderAddressPublicKey: '<payment public key>',
  bidderTokenReceiveAddress: '<ordinals address>',
  quantity: 2,
  metaType: 'ordinals',
  bidExpiry,
  timestamp,
  signedBiddingMessage: '<base64 signature of the message built from these values>',
};

Use "metaType": "runes" for a rune bid. The response is data.message, "Successful collection bid!". For ordinals, a bid with a quantity of N places N bids at price each. One request places at most 1,000 bids: a larger one returns 400 with errorCode: "COLLECTION_BID_TOO_MANY_ORDERS", and details.limit and details.requested.

A signed message places one bid. To place another, even an identical one, build and sign a new message with a new timestamp. Sending a message that has already placed a bid again returns 400 with errorCode: "COLLECTION_BID_SIGNATURE_USED". To retry a request that failed, also sign a new message. If it timed out or returned a 5xx error, check GET /address/bids first: the bid may have been placed.

You can place one bid every 500 milliseconds per bidderTokenReceiveAddress, across all collections. A bid on several traits counts once per trait. A faster request returns 429 with errorCode: "SUBMIT_BID_RATE_LIMITED" and retryAfterMs, how long to wait.

Trait bids

To bid on items with a given trait, add attributes to the POST /bid/place body:

"attributes": [{ "trait_type": "Background", "value": "Orange" }]

Each trait must exist in the collection, and each one gets its own set of quantity bids and its own bid pool. One request takes at most 50 traits (more returns 400 with errorCode: "COLLECTION_BID_TOO_MANY_ATTRIBUTES"), and places at most 1,000 bids: quantity times the number of traits. The older single attribute object is still accepted, and counts as one trait.

A trait bid signs its traits. Its message is the colon-joined line from step 3, then a line with the bid type, then a line with the traits as JSON, joined with newlines (\n):

<bidderAddress>:<bidderAddressPublicKey>:<bidderTokenReceiveAddress>:<price>:<quantity>:<bidExpiry>:<collectionSlug>:<timestamp>
Type: ordinals
Attributes: [{"trait_type":"Background","value":"Orange"},{"trait_type":"Eyes","value":"Laser"}]
  • Type: is followed by the metaType you send.
  • Attributes: is followed by the traits you send (or the single attribute, as a list of one), sorted by trait_type and then by value, and written as JSON with no spaces. Each trait is written as trait_type then value, and nothing else.
  • Strings are compared by UTF-16 code unit, as JavaScript's < does, and escaped as JSON.stringify escapes them: quotes and backslashes are escaped, non-ASCII characters are written as they are. In Python, for example, that takes json.dumps(traits, separators=(",", ":"), ensure_ascii=False).
  • Add the two lines only when the bid has at least one trait. With an empty attributes list and no attribute, sign the colon-joined line alone.

In JavaScript, which compares and escapes strings the way Satflow does:

// attributes: the traits you send. The other values are the ones from step 3.
const compare = (a, b) => (a === b ? 0 : a < b ? -1 : 1);
const sorted = [...attributes].sort(
  (a, b) => compare(a.trait_type, b.trait_type) || compare(a.value, b.value)
);

const message = [
  `${bidderAddress}:${bidderAddressPublicKey}:${bidderTokenReceiveAddress}:${price}:${quantity}:${bidExpiry}:${collectionSlug}:${timestamp}`,
  `Type: ${metaType}`,
  `Attributes: ${JSON.stringify(sorted.map(({ trait_type, value }) => ({ trait_type, value })))}`,
].join('\n');

Send the traits in the request in any order. A trait bid whose signature covers only the colon-joined line returns 400 with errorCode: "COLLECTION_BID_ATTRIBUTES_NOT_SIGNED".

Collection bids also work on the inscription-number ranges sub1k, sub10k and sub100k, used as the collectionSlug. Trait bids are not available on those.

4. Check your bids

GET /address/bids?address=<ordinals address> returns your open bids in data.results. Collection bids have type: "collection" (rune bids type: "rune"), and trait bids carry their attribute.

Cancelling a collection bid

Get a wallet credential for your ordinals address (see Signing the Satflow Challenge), then call POST /cancel with makerAddress (your ordinals address), orderType: "bid", the credential as signature, and the bid's id as _id (or several as _ids). GET /address/bids returns the id as bid_id, an object whose $oid field is the id to send.

Withdrawing from the bidding wallet

  1. Call POST /intent/withdraw with userAddress (your payment address) and amount in sats. The response has the unsigned PSBT in data.unsignedPSBTBase64. Withdrawals always go to your payment address.
  2. Sign every input with the key of your payment address.
  3. Send the signed PSBT to POST /withdraw as signedWithdrawPSBT. The response has the txid.

Bids on a single inscription

A bid on one inscription is paid from your payment address, not the bidding wallet:

  1. Build it with POST /intent/bid: price (at least 10,000 sats), inscriptionId, bidderAddress (your payment address), bidderTokenReceiveAddress (your ordinals address) and bidderPublicKey. The unsigned PSBT is in data.bidder.unsignedBiddingPSBTBase64.
  2. Sign every input except input 0 (the inscription, which the seller signs when accepting) with your payment key, sighash ALL. Satflow's fee comes out of the seller's proceeds, so it does not add to your cost: you pay price, the postage of the inscription output and the network fee.
  3. Submit it to POST /bid/place with price, inscriptionId, bidderAddress, bidderTokenReceiveAddress and the PSBT as signedBiddingPSBT. Send the PSBT unchanged apart from your signatures.

The bid stays open until you cancel it, you spend the coins that fund it, or the inscription moves.