Skip to main content
Code for the arkade:<asset> → arkade:<asset> route: BTC against an asset such as USDT. Exactly one side is BTC; the client refuses an asset-for-asset swap.
Prerequisites:
  • an initialized Arkade Wallet on mutinynet: Create Your Wallet, with { isMainnet: false } and the mutinynet operator, https://mutinynet.arkade.sh. The mainnet registry lists no asset market today, and the mutinynet registry lists BTC/USDT.
The client reads the solver registry for the wallet’s network. Create it, and name the two assets:
In Node, use the storage and EventSource setup from Swap Client instead.

Discover and Price

The mutinynet BTC/USDT card lists a price feed and fee_bps and no relay, so quote calculates the terms from them and contacts no solver. The card reserves no inventory: only a fill commits the solver. To limit which registries and markets can price a swap, set policy.allowedRegistries and policy.selectMarket.
Mind the units. amount is a bigint in the asset’s smallest unit: sats for BTC, hundredths for USDT (2 decimals). amountOn says which side it pins. Convert what a user types with Amount.parse (Swap Client). amountOn: 'take' works when you buy an asset with BTC. Selling an asset for BTC through a card that names a relay is exact-in only, so set the amount on the give side there. Keep what you receive within the card’s bounds. The solver checks its bounds against what it pays out, which is what you receive. With amountOn: 'give', the client does not check them: a swap outside them is funded, the solver does not fill it, and you must cancel it to get the deposit back. With amountOn: 'take', the client checks them and throws UnsupportedRoute before anything is funded. A card that names a relay accepts only amountOn: 'give' when you sell an asset for BTC. Show the deposit, receive amount, and fee before funding. accept does not check a fee ceiling: compare quote.fee.amount with your limit yourself, or use exchange with maxFee. A quote expires. After that, accept throws QuoteExpired, and it never gets a new quote for you. Ask again. An open quote can block your next one. Until a quote is funded or expires, the reference solver can refuse another quote from the same wallet. quote then throws SwapRefusal with the reason quote_conflict. The reference solver’s quotes last about 30 seconds by default. If you show a price and the user does not fund it, wait until quote.expiresAt (unix seconds) before you ask again.

Fund

accept registers the covenant with your wallet’s contract manager, saves the record, and funds the swap. Registration marks the contract genericallySpendable: false, so send, settle, and background renewal do not spend a live swap.
accept saves before it funds. If you retry accept with the same quote, it resumes the stored swap and does not fund twice. To get a quote, check the fee, and accept in one call, use exchange. The fee is charged in the asset you receive, so maxFee must name that asset. A ceiling in any other asset throws. Nothing is funded until the fee passes:
You can swap your whole balance on this route. The fee comes out of what you receive, so you do not need to keep anything back to pay it. On Lightning and onchain routes, the fee comes out of what you give, so the client refuses a whole-balance swap there. Selling an asset can cost its carrier. Every asset sits on 330 sats of BTC, called the carrier. When you sell an asset, your deposit holds the asset and its carrier.
  • A card that names a relay: the reference solver keeps the carrier, and quote.fee counts it. For example, a sale of 4.00 of an asset showed a fee of 345 sats, and only 15 of those were the spread. Set maxFee in BTC and above 330 sats.
  • A card without a relay: the fill pays the carrier back to you with the BTC, and quote.fee leaves it out.

Track and Cancel

client.onUpdate reports each swap’s status as an outcome. The client watches the wallet’s contract events and persists before it notifies:
open means funded and waiting for a solver. filled means done: the asset is in your Arkade wallet. After a wallet restore, client.ready rebuilds the records by scanning the wallet’s history for offer deposits. An unfilled swap has no fill deadline, but the virtual output that holds your deposit still expires with its batch. If the deposit stays unfilled past that, the operator sweeps it, and the swap reports needs_recovery. Recover a Swept Swap brings it back. To take back an unfilled deposit, cancel the swap:
cancel submits the cancel transaction to Arkade without the solver’s signature, and writes the cancelling → cancelled transition itself. Cancellation races a fill, and losing that race is not an error. If the deposit was already spent, the client checks which contract path spent it and reports filled: the swap completed. If it cannot tell, cancel returns needs_recovery, and the swap stays at cancelling. Nothing was swept, so recover changes nothing and throws SwapDriveRefusedError (nothing-swept). Show the swap to a person, and let the next pass check it again.

Swap One Asset for Another

The swap client and the reference solver serve only pairs with BTC on one side. To exchange one asset for another, make two swaps through BTC: sell the first asset for BTC, then spend exactly that BTC on the second asset. Each asset needs its own market with BTC.
  • You pay the spread twice.
  • Ask for the second quote after the first swap fills. While a quote from your wallet is open, the reference solver can refuse another one.
  • If the second swap does not fill, the BTC from the first stays in your wallet. Nothing swaps it back.

Want to learn more?

Markets and Quotes

The model underneath: how quotes are requested, priced, and committed.

Asset Swaps

The underlying contract: pricing, funding, lifecycle, and cancellation.

RFQ Protocol

Quote wire family, transport, settlement, and recovery requirements.