raresats.net API
Guides View as Markdown

List a UTXO

Listing is three steps: prepare a PSBT, sign it with SIGHASH_SINGLE|ANYONECANPAY (0x83), submit it. Your key never leaves your wallet or script; the server keeps only the signature, and never shows it to anyone.

Before you start #

  • The UTXO must be confirmed, hold tracked rare sats of a market row, and carry no inscription or runes. Find yours with GET /api/address/{address}.
  • It must belong to a Taproot, native SegWit or nested SegWit address. Legacy (1…) is refused with legacy_address.
  • Choose a price in sats: what you receive. It must be above the UTXO's own value and at least 546. Buyers pay 2.5% on top; you pay nothing.

1. Prepare #

POST /api/market/listings/prepare with your address, its public key and the UTXOs (up to 50 in one PSBT, one signature prompt):

Request
{
  "seller_address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
  "seller_public_key": "0283dfe85a3151d2517290da461fe2815591ef69f2b18a2ce63f01697a8b313145",
  "items": [{ "outpoint": "8b09d6914ae17431940ce8b6cadae378a5d9b3c90c73c3a5b0b8c8e7edc9e86a:0", "price": 94200 }]
}
  • seller_public_key: for Taproot, the internal (untweaked) key, x-only (32 bytes) or compressed (33 bytes); for SegWit, the compressed key. Wallets return it with the address.
  • payout_address (optional) is where the price goes; it defaults to seller_address. Xverse-style wallets usually pay out to their payment address.

The answer is the PSBT, the inputs to sign (sighash: 131 = 0x83) and what each listing will show:

Response
{
  "psbt": "cHNidP8BAF4CAAAAAWroye3n…",
  "sign": [{ "index": 0, "address": "bc1p4qhj…fqzwh", "sighash": 131 }],
  "items": [{ "outpoint": "8b09…e86a:0", "value": 6280, "price": 94200, "unit_price": 14, "row": "block9", "count": 6280 }]
}

The PSBT pairs input i (your UTXO) with output i (your price to your payout address). Version 2, locktime 0, sequence 0xfffffffd. Check those outputs before signing.

2. Sign with 0x83 #

Sign every input in sign with sighash 0x83 (SIGHASH_SINGLE|ANYONECANPAY). Your signature then commits only to your UTXO and to your payment output: a buyer can add their inputs and outputs, but cannot change what you receive. Do not finalize or broadcast.

  • UniSat: unisat.signPsbt(hex, {autoFinalized: false, toSignInputs: [{index, address, sighashTypes: [0x83]}]}).
  • Xverse (Sats Connect): signPsbt with signInputs: {[address]: [indexes]} and broadcast: false; the sighash comes from the PSBT input's sighashType, which prepare set to 0x83.
  • Your own key: the Node script below.

3. Submit #

POST /api/market/listings with {"psbt": "<signed base64>"}. The server checks every rule again, verifies each signature against the chain's amount and script, and answers your new listings, active:

Response
{ "listings": [{ "id": "3324d08569806818", "status": "active", "price": 94200, "unit_price": 14, "row": "block9", "count": 6280, "…": "…" }] }

To change a price, list the same UTXO again: the new listing replaces the old one (now cancelled, its signature deleted). To take it off sale, cancel it.

A complete Node example #

A script that lists a UTXO of a Taproot key: Node 18+, npm install bitcoinjs-lib tiny-secp256k1 ecpair.

list.mjs
// node list.mjs <txid:vout> <price in sats>
// WIF: the private key (mainnet WIF) of the Taproot address holding the UTXO.
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import { ECPairFactory } from 'ecpair';

bitcoin.initEccLib(ecc);
const ECPair = ECPairFactory(ecc);
const BASE = process.env.RARESATS_URL ?? 'https://raresats.net';
const [outpoint, price] = process.argv.slice(2);

const key = ECPair.fromWIF(process.env.WIF);
const internalKey = key.publicKey.subarray(1, 33);
const { address } = bitcoin.payments.p2tr({ internalPubkey: internalKey });

async function post(path, body) {
  const response = await fetch(`${BASE}${path}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body)
  });
  const json = await response.json();
  if (!response.ok) {
    throw Object.assign(new Error(`${path}: ${response.status} ${json.code}: ${json.error}`), { code: json.code });
  }
  return json;
}

// 1. Prepare: the server checks the UTXO and builds the PSBT.
const prepared = await post('/api/market/listings/prepare', {
  seller_address: address,
  seller_public_key: Buffer.from(key.publicKey).toString('hex'),
  items: [{ outpoint, price: Number(price) }]
});

// 2. Check what you sign, then sign every input with 0x83 (SINGLE|ANYONECANPAY).
const psbt = bitcoin.Psbt.fromBase64(prepared.psbt);
psbt.txOutputs.forEach((output, i) => {
  if (Number(output.value) !== prepared.items[i].price || bitcoin.address.fromOutputScript(output.script) !== address) {
    throw new Error(`output ${i} does not pay the price to ${address}`);
  }
});
const signer = key.tweak(bitcoin.crypto.taggedHash('TapTweak', internalKey));
for (const { index, sighash } of prepared.sign) {
  psbt.signInput(index, signer, [sighash]);
}

// 3. Submit: the listing is live.
const { listings } = await post('/api/market/listings', { psbt: psbt.toBase64() });
for (const listing of listings) {
  console.log(`${listing.id} ${listing.outpoint}: ${listing.price} sats, ${listing.unit_price} per sat of ${listing.row} x${listing.count}`);
}

For a native SegWit (P2WPKH) key, use bitcoin.payments.p2wpkh({ pubkey: key.publicKey }) for the address and sign with key itself (no tweak).

Errors you may meet #

CodeWhat to do
not_owner, key_mismatchThe UTXO or the key is not the address's.
untracked, no_rowIt holds no rare sat a market row tracks.
inscribed, runesOnly plain rare sats are listed.
unconfirmed, unindexed, indexer_behind, fork_behindWait for a confirmation and the indexers; try again in a minute.
price_too_lowPrice above the UTXO's value and at least 546 (and the payout's dust limit).
spent, changed, utxo_mismatchThe UTXO moved, or the PSBT's amount is not the chain's. Prepare again.
pendingIt is being bought right now.
bad_signature, bad_sighash, missing_signatureSign every input in sign, with 0x83.

Every code: Errors.