Skip to main content
A negotiated quote commits live amounts, keys, timeouts, and route-specific construction data before settlement begins. This page defines the wire family that carries those quotes — one family for every route. Served today: Lightning send and receive, onchain send, and asset swaps whose solver card names a relay. The open-source reference solver, intent-solver, serves them. An asset card without a relay advertises only a feed, and the client prices those swaps itself (Asset Swaps). See Implementation Status for what each deployment serves.

Quote Lifecycle

Addressed and Published Requests

Addressed is the shipped path — build against it. Published mode is still in development: no client publisher exists, the reference deployment bids only on Lightning sends, and its payloads are not specified below. The client work is tracked in ts-sdk#725. Neither a request nor a bid locks funds. A bid is signed and attributable but not bonded. The signed quote returned by the selected solver is the binding RFQ message.

Relay

RFQ messages may travel through Nostr relays. Client and solver each dial out; neither needs to accept inbound connections. A relay transports messages but does not select a solver, aggregate quotes, or touch funds. The negotiation kinds sit in NIP-01’s ephemeral range: relays deliver them to live subscribers and store nothing, so an expired negotiation leaves no public record. The cost is no store-and-forward. A request sent while the solver is offline is dropped: the swap client gives up after 30 seconds, and the app can request again. The card kind is addressable: a relay keeps only the current card per solver and network. Kind numbers remain provisional.

Message Family

Every payload carries the envelope {"v": 1, "type": …}. Addressed payloads are transport-independent. The reference service accepts HTTP and Nostr traffic; clients can implement another transport while preserving the same payloads.

Payload Schemas

Field names below are exact. The reference solver’s request schemas are .strict(): an unknown field, a misspelled one, or a missing required one refuses the whole request with unsupported_payload. A wrong name and an absent field fail identically. Responses are the reverse: a client ignores response fields it does not know, so a solver can add one without a version change. These are the addressed-mode payloads. rfq_open and rfq_bid are work in progress and are not specified here — see Addressed and Published Requests.

Encodings

Amounts depend on the route:
  • Asset routes take a canonical decimal string only: ASCII digits, with no sign, decimal point, exponent, or leading zero.
  • BTC corridor routes take a request amount as a canonical decimal string or a JSON integer up to 2^53 − 1, and a solver must accept both. @arkade-os/swap sends a string. Send a quote’s amounts as JSON integers: the client reads both forms, but clients before 0.1 refuse a string BTC amount in a quote.
Both scripts commit to ripemd160(sha256(P)), derived locally from payment_hash on each side; the hex on the wire is the sha256 digest alone. Client-supplied keys are x-only. Solver-supplied keys (solver_pubkey, htlc_pubkey) may also arrive as 33-byte compressed keys, and the reference client drops the prefix before using them.

Request Envelope

rfq_id is the client’s, not the solver’s — generate it before sending and correlate every reply against it. A reply whose rfq_id differs is a reply to a different negotiation and must not be read as this one’s answer. The BOLT11 send profile is the one route that omits amount: the invoice fixes it, so the request pins amount_side to to and restates nothing. A quote echoes the fixed side exactly. A from request must come back with from_amount equal to amount, and a to request with to_amount equal to amount; on the BOLT11 send, to_amount must equal the invoice amount. The client refuses any other quote as a quote for a different trade.

arkade:BTC → lightning:BTC

arkade:BTC → onchain:BTC

lightning:BTC → arkade:BTC

P never travels in the clear. The solver receives the hash, plus the sealed packet only when the client sends one; it learns the preimage only when a claim publishes it. A client that sends no packet must claim the lockup itself.

Routes whose endpoints both use arkade

The swap client sends this request to any asset card that names a relay and a discovery key, and intent-solver answers it for the markets in its ASSET_MARKETS setting. The pair names the asset: each asset leg is arkade: followed by the asset id as 68 lowercase hex characters, and a BTC leg is arkade:BTC. The profile names the client’s position in the swap contract.
maker_* names a position in the contract, not a role — see The maker Fields. Amounts on these routes are canonical decimal strings. The quote carries no refund_locktime, because neither fulfill nor cancel has a timelock.

Quote

rfq_id and pair must equal the request’s. refund_locktime is present on every HTLC-class quote and absent where both endpoints use arkade. The spread between from_amount and to_amount is the complete cost — there is no fee field. An asset quote can also carry a top-level carrier_sats: the dust sats an asset deposit sits on. The amounts already include it, so it is not a fee. The quote profile carries the route’s construction data: Which of these bind and which are compared is set out in Quote Boundary and, per direction, in Derive, Never Accept.

Refusal

reason comes from a closed set: unsupported_pair, unsupported_payload, amount_out_of_range, exposure_cap, invoice_expired, quote_conflict, pricing_unavailable, rate_limited. Treat an unrecognized reason as a generic decline. The set may grow, and a refusal is never an error to retry blindly. A refusal can also carry detail fields: error_code, from a second closed set (for example invoice_wrong_network or invoice_cltv_too_large), and field, actual, expected, limit, and unit (blocks, characters, or sats). They explain the refusal; they do not change how to handle it.

Status

state is one of the wire states below. Receipts appear only in settled: preimage on corridor routes, fill_txid on asset routes. Status is best effort: a timeout, a missing response, or an unknown state means unknown, never failed. For an rfq_id it does not know, the reference solver answers with an rfq_refusal on Nostr and a 404 on HTTP, and the reference client reads both as “no status”.

Transport Framing

The payloads above are what a transport carries; neither transport changes a field. A 4xx carrying an rfq_refusal body is the refusal path, so the status code alone cannot decide whether to read the body. Parse it, and fall back to the status code only when the body is not a payload of this family. On Nostr the client signs with a transport key it may generate fresh per negotiation; nothing in the protocol needs a stable client identity, since the quote binds to the contract rather than to who asked for it. Replies arrive on a subscription filtered to that key and are correlated by rfq_id. The example below sends an arkade:BTC → lightning:BTC request over Nostr by hand and prints the reply. @arkade-os/swap does all of this for you; the example is for a client in another stack. Take the relay and the solver key from the solver’s card. Install the one dependency:
A real client also needs a timeout: the solver may be offline, and the relay drops a request that no one receives.

RFQ Wire States

settled, refused, expired, refunded, and stuck are terminal. On asset routes the fill pays both sides in one transaction, so the reference solver reports settled with no filled step, and it reports a quote that expired unfunded as refused. The swap client monitors funded swaps from chain evidence and reports its own outcomes rather than consuming these wire states; its failed outcome is client-side reporting.

Quote Boundary

Economic commitments stay at the top level. Settlement construction data stays inside profile. Profile fields are also checked against client-owned inputs such as an invoice, payment hash, destination, and network. Recheck time-sensitive conditions immediately before value moves.

Transport Requirements

Regardless of transport implementation:
  • reconnect after both connection errors and clean closes;
  • restore live subscriptions on a new connection;
  • bound and persist or reject outbound events queued while disconnected;
  • correlate every response with the expected RFQ identifier and route;
  • treat a status timeout as unknown, not failed.
Contract and destination-network evidence remain authoritative.

Persistence Requirements

@arkade-os/swap keeps the recovery record below in the repository the app gives it. A client in another stack must persist it before the route’s first enforceable commitment. At a minimum, retain:
  • route;
  • swap identifier or RFQ identifier;
  • encoded terms and verified solver identity;
  • derived script or contract data;
  • funding transaction identifier;
  • expiry and refund timing;
  • wallet-owned key or preimage derivation data;
  • destination-network identifiers required for reconciliation.
Some generated secrets do not exist anywhere else. Losing them can remove an interactive refund or claim path.