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
amountas a canonical decimal string or a JSON integer up to 2^53 − 1, and a solver must accept both.@arkade-os/swapsends 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.
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:
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 insideprofile.
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.
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.