Skip to main content
One swap client lets an Arkade wallet pay and receive over Lightning, pay and receive onchain, and exchange assets. This page sets the client up once: storage, asset ids, solver discovery, errors, and how to follow and recover a swap. Lightning Payments, Onchain Payments, and Exchange Assets show each flow.
You need an Arkade wallet: Create Your Wallet. The client takes no server URL. It gets the operator’s details, chain data, and broadcasting from the wallet.

Create the Client

Creating the client does no work. It opens no connection, reads no wallet state, and touches no storage until a call needs them. You can create it anywhere, such as in a component body, at no cost.
Await client.ready once at startup. It reads the repository, finds the swaps that were in progress when the process last stopped, and resumes them. If you restore a wallet, run wallet.restore() before you create the client. The client rebuilds swaps from the wallet’s history, so it finds only what the history holds when it starts. Swaps saved by an older 0.0.x client in the same repository are picked up automatically. Keep nostr-tools in the install. It is an optional peer dependency, so package managers skip it. The client uses it to reach solvers over Nostr: every Lightning and onchain quote needs it, and discovery reads solver cards with it.

Choose Storage

The repository stores each swap’s record and secrets. There is no default storage and no in-memory fallback, because a swap with no stored record can be lost. Without a repository, accept throws MissingCorridorDep. The React Native backends add no dependency. Each takes a database handle you already opened. In Node, createNodeSqlExecutor opens the file for you. It has its own entry point, @arkade-os/swap/node, because it is the only one that imports Node built-in modules. Open one executor per file, for the life of the process. A second executor over the same file can interleave two writes. swapDatabasePath names one file per network in the platform’s config directory. In Node the wallet needs SQLite storage (Storage Repositories) and an EventSource too. This client keeps the wallet and the swaps in two SQLite files, and loads the mnemonic from the ARKADE_MNEMONIC environment variable:
Disposing the client does not close the repository or the database files, so close them yourself. Records are stored whole, so a field you add survives a save and a load. Keep added fields JSON-safe: the SQLite and Realm backends store JSON, so a Date comes back as a string and a bigint throws on save.

Name Assets and Amounts

An asset id names an asset and the rail it settles on: <rail>:<network>/<asset-namespace>:<asset-reference>, a format called CAIP-19. The rail is arkade, bolt11, or bitcoin, and BTC has one id per rail. On mainnet, arkade:bitcoin/slip44:0 and bolt11:bitcoin/slip44:0 are the same coin on two rails, and one id does not replace the other. Test networks use slip44:1 for BTC. Build ids with btcOn and arkadeAsset, not as strings. This example names BTC and USDT on mutinynet, where the registry lists a BTC/USDT market:
canonicalAssetId passes a valid id through unchanged. It throws when no asset matches the ticker, or when two different assets do. The client counts amounts as bigint in the smallest unit, such as sats for BTC. Amount converts to and from the decimal strings your interface shows:

Find Markets

A market is a pair of assets that a solver quotes, listed on the solver’s card. Discovery works with no configuration: the client reads the solver registry for the wallet’s network. To add a card that the registry does not list, pin it with discovery.localCards. The mainnet registry does not list the Lightning preview solver, so this example pins its card, saved as beta-solver.card.json from Lightning Payments:
When you pin a card, your application trusts that solver and the connection details on its card. The card’s sig signs them with its discovery_pubkey. discovery.registryUrl points the client at another registry. A pinned card adds to a registry. It does not replace one. With registryUrl: null, discovery stops before it reads localCards, so nothing resolves. To work with no network access, pass a fixed market list: discovery: { snapshot }. You do not need client.markets() to swap, because quote finds the market itself. Use it when your interface must show which pairs are available before anyone asks for a price. client.resolve(input) checks what the current market list would serve without sending anything to a solver. Use it to reject a route before an invoice or an amount leaves your application. It reads only the markets the client already holds: pinned cards, markets that client.markets() or a quote loaded, the repository’s cache, or discovery: { snapshot }. With none of these, it throws DiscoverySnapshotUnavailable.

Handle Errors

Every swap error is thrown before money moves. After funding, the client reports a problem as an outcome instead. The swap errors are sixteen classes, each named for a condition, all exported from the package root. SWAP_ERROR_NAMES lists them, and isSwapError matches any of them:
Two errors come up most:
  • UnsupportedRoute: no market the client holds serves the route at that amount. The client contacted no solver. Check the registry or pin a card.
  • SwapRefusal: the solver declined, for example for an amount outside its bounds or for lack of inventory. Nothing was committed, so show the reason or ask again.
SwapDriveRefusedError, which recover and start throw, is not a swap error, and isSwapError does not match it.

Follow a Swap

Every route reports progress with the same Outcome values. When you subscribe, you first get the current outcome of every swap the client knows, then each change. A swap that finished before you subscribed still shows up:
onUpdate returns a function that unsubscribes. To read swaps without subscribing, call client.swaps(), which takes an optional filter such as { outcome: 'open' }. Which outcome means done depends on the route: On any route, accept(quote, { validUntil }) throws if it cannot fund the swap before validUntil (Unix seconds), and the swap reports cancelled: nothing was funded. The values differ because the lockup holds different money on each route. The lockup is the Arkade contract that holds a swap’s money until it is claimed or refunded. On a send, it holds your money: if it is spent without a claim, your money comes back, and refunded always means that. On a Lightning receive, the lockup holds the solver’s money, so the same spend means your payment never arrived. That outcome is lapsed: no payment arrived before the window closed, or the solver took back a lockup that was not claimed in time. A deposit from a Bitcoin address is the exception among receives: you fund it first, so if it fails, your deposit comes back as refunded. To take back an unfilled asset swap, see Track and Cancel. Lightning and onchain swaps end with a claim or a timelocked refund instead.

Recover a Swept Swap

needs_recovery is always reported, never retried silently, and never final. It has two causes, and each needs a different response:
  • Blocked. Nothing moved, and the money is still in the lockup. swap.blockedReason says why. The client checks again on every pass, and the swap resumes when the cause clears. Show the reason.
  • Swept. The swap’s outputs expired and the operator swept them. client.recover(swapId) renews them. This is the one manual step a swap can need.
recover tells the two apart. On a blocked swap it changes nothing and throws SwapDriveRefusedError, so you can call it on every needs_recovery and show the reason when it refuses:
SwapDriveRefusedError has a reason: nothing-swept (the blocked case above), refund-window-open, readonly, no-recovery-support, or unknown-swap.

Set the Client Policy

policy sets how much the client does on its own, and the most a swap may cost. The drive is the background loop that claims, refunds, and recovers swaps. policy.drive sets how much it does on its own: You can call start() and stop() more than once. stop() pauses the client: actions in progress finish, records and contract registrations stay, and you can start it again. Disposing the client ends that instance, but records and wallet registrations remain. A new client on the same repository resumes the work. In "readonly" mode, start() and recover() throw SwapDriveRefusedError. A policy fee ceiling can only make limits stricter. Before anything is funded, the client applies the lower of policy.maxFee and the call’s own maxFee:
Each ceiling names an asset. If it names a different asset than the quote’s fee, the call throws a plain Error, not one of the sixteen swap errors. The client does not convert between assets. Asset swaps charge the fee in the asset you receive, so a BTC ceiling refuses an asset swap from BTC to USDT.

Want to learn more?

Lightning Payments

Pay Lightning invoices from an Arkade wallet, and receive Lightning payments into it.

Onchain Payments

Pay Bitcoin addresses and take Bitcoin deposits through a solver.

Exchange Assets

Swap bitcoin for other assets, settled through Arkade.

Markets and Quotes

How quotes set the terms, and which contract settles each route.

Corridor Swaps

Routes with one endpoint outside Arkade, settled by paired HTLCs.

Asset Swaps

The swap flow for exchanges whose two sides settle through Arkade.