fee_bps. Either
way the user funds the derived contract, and any compatible solver fills it.
The contract records what the user gives and the minimum result a fill must
deliver. It stays open until a solver fills it or the user cancels.
Lifecycle
The deposit is a virtual output at the covenant script, and every status is a reading of that one virtual output. The covenant has three paths:fulfill, cancel, and exit. A deposit that moved took one of them, and
which one is read off the spending transaction’s tapleaf, not inferred from
what the transaction moved.
The swap client’s
onUpdate reports the client outcome as outcome and the
status as detail.status.
The path is the evidence, not the amounts. An asset swap’s cancel takes
the asset out of the covenant and hands it straight back, so by amounts it
nets to zero and reads exactly like its fill. A spend that took none of the
paths, or one that cannot be fetched yet, stays unclassified: the record
keeps its status and a later read decides it.
cancelling is local and provisional. client.cancel writes it before it
broadcasts, so a crash between the submit and the record leaves a marker rather
than a swap that still reads pending. It is not evidence that a cancel
happened: a fill can win the race, and whichever path lands sets the status.
recoverable is the one status no spend produces. The deposit was swept —
the batch it sat in expired before any path was taken — so it is a
recoverable virtual output like any
other: still yours at that script, no longer exitable unilaterally, and
recovered by settling it rather than through the covenant. The restore scan
that client.ready runs writes this status, because the live watch reports
spends and a sweep is not one.
Funding the Swap
client.accept derives the non-interactive swap contract from the approved
terms, registers it, records the swap, and funds the derived address. The
funding transaction carries an extension packet of type 0x03 with the
encoded offer. Solvers watching the Arkade transaction stream read that packet
to learn the funded output’s terms.
The swap record keeps the encoded offer and the funding transaction ID.
Identical terms derive the same address, so the address alone does not
identify one deposit.
What the Contract Enforces
The non-interactive swap contract binds the asset identity and minimum amount the fulfillment must deliver. Both legs settle in one Arkade transaction. The covenant reads exactly one output. A fill is any transaction whose output 0 pays your script at least the amount of the asset you named; everything else about it — how many inputs the solver brings, where its change goes — is the solver’s business. The deposit side is never inspected, which is why the same contract carries BTC, an asset, or an asset-for-asset swap. Wallets built on@arkade-os/swap create only swaps between BTC and an asset.
The 0x03 packet enforces the same limit: its encoder and decoder refuse an
offer that does not name exactly one of wantAsset and offerAsset.
Read the fill’s two inputs together and the swap is visible: the solver’s
coins pay you, your deposit pays the solver, and neither leg can happen
without the other because they are the same transaction.
The three ways out are deliberately asymmetric. A fill needs no signature
from you — the covenant is the constraint, so a solver cannot take the
deposit without paying output 0, and you can be offline while it happens. A
cancellation needs no signature from the solver, so taking the deposit back
never waits on a counterparty. An exit needs no one else: after a relative
timelock, you alone can take the deposit back through a unilateral exit, for
example if the operator is unavailable. The SDK builds the exit path by
default, with the operator’s unilateral exit delay.
Neither fulfill nor cancel carries a timelock, so the contract sets no fill
deadline: an unfilled deposit keeps its place at the swap address on the terms
it was funded with, and there is no expired state to unwind. The virtual output
holding it still carries its batch expiry — one left resting past that is swept
rather than filled or cancelled, which is the recoverable status above.
The contract does not guarantee:
- that a solver has sufficient inventory;
- that the feed represented a fair or current market price;
- that the swap fills within a particular time;
- that an asset issuer redeems or honors the asset.
fulfill path is emulator-enforced: the operator runs the covenant’s
Arkade Script before it signs. The path commits to the key of the operator’s
cosigner that emulates Arkade Script. That key is a per-network constant pinned
in @arkade-os/sdk (mainnet, mutinynet, and regtest), not read from the
operator. On a network with no pinned key, the SDK
throws instead of guessing.
The swap client’s optional emulatorPubkey setting (33-byte compressed hex)
overrides the pin. Review
Trust and Limitations
before using the flow with value.
Standing Offers
A funded contract does not wait on the person who funded it. It names the asset, the minimum amount, and the script a fill has to pay, and anything that delivers those three settles it — from any solver, at any time, with the funder offline. An exchange would call terms fixed in advance a limit order, and one waiting to be taken a resting order. This is that, held as a contract within Arkade instead of a row in an order book. The delivery and the spend are the same transaction, so there is no interval in which the contract is open and the payment is owed: a fill that does not pay the named terms is not a fill, it is an invalid transaction. A fill is all-or-nothing — partial fills would need a contract that pays a remainder back into itself, which this one does not do.Price Without Aggregation
The terms fix a ratio: what the contract holds, against what a fill must deliver. That ratio is the price. Solvers watch resting contracts and take the ones that beat their own pricing, leaving the rest. Nothing aggregates them. Each contract is independently enforceable, so no venue holds the orders, matches them, or has to stay solvent for one to settle — the reason the pattern is worth having rather than custody attached to the matching. Today the ratio comes from the quote, not from you. You approve the quoted terms before funding, so a contract rests at a price that was current when you funded it. Choosing your own ratio, an offer that waits for the market to reach it, is future work, as is any surface that ranks resting contracts.Discovery and Pricing
Solver cards advertise supported pairs, amount bounds, indicative fees, and a price feed for cross-asset markets. Applications choose which cards and registries they accept. The card and feed are advisory:- they do not reserve solver inventory;
- a feed response is not a signed quote;
- the application applies its own freshness, tolerance, and eligibility policy;
- the user reviews the resulting deposit, receive amount, and fee before funding.
The maker Fields
The covenant programs and the Offer type carry maker* identifiers —
makerWP, makerPkScript, makerPublicKey. They name contract positions,
not roles: the side that funds the swap and receives the fill. Arkade Intents
deliberately avoids maker and taker as participant vocabulary; the parties are
the user and the solver.
Sources
- Asset swap settlement:
intent-solverRFQ protocol, § 7.2 @arkade-os/swappackage- Swap Contract: the contract source and the transaction each path builds