Banco is a non-interactive swap protocol on Arkade. A maker publishes a swap offer and goes offline; any taker can fulfill it asynchronously. Atomic settlement is enforced by an Arkade-script covenant on the maker's VTXO — no signature from the maker is required at spend time.
Banco supports BTC ↔ asset, asset ↔ BTC, and asset ↔ asset swaps, with optional partial fills via a price-ratio covenant.
Conventional atomic-swap constructions (HTLC, scriptless scripts, PTLC) need both counterparties online at the same time. Banco moves the swap predicate into a covenant on the maker's VTXO so that the maker's only action is to fund and walk away. A taker can later spend that VTXO if — and only if — their transaction pays the maker the wanted amount in the wanted asset, in the same Arkade transaction that releases the maker's funds.
| Party | Role | Trust |
|---|---|---|
| Maker | Publishes an offer, funds the swap VTXO, goes offline. | Trustless. |
| Taker | Discovers an offer and submits a fulfillment. | Trustless. |
| Operator | Runs the Arkade infrastructure and cosigns the fulfillment. | Liveness only — cannot steal or redirect funds. |
| Emulator | Validates the covenant at fulfillment time and cosigns. | Liveness only — the covenant is enforced by Arkade script regardless of who validates. |
No party custodies the maker's funds. The covenant binds the spending transaction to pay the maker a specific amount of a specific asset to a specific scriptPubKey. The Operator or Emulator can refuse to cosign (denial of service) but cannot redirect funds.
sequenceDiagram
autonumber
participant M as Maker
participant N as Arkade
participant T as Taker
participant I as Emulator
participant O as Operator
M->>M: derive swap VTXO (covenant taptree)
M->>N: fund swap VTXO + embed offer in extension packet
Note over M: maker goes offline
T->>N: discover offer (by funding txid or hex)
T->>T: build fulfillment tx satisfying the covenant
T->>I: submit for covenant validation
I->>O: cosign
O->>N: atomic settlement
N-->>M: paid wanted amount at maker address
N-->>T: receives offered funds (or fillAmount portion)
- Offer creation. The maker computes a covenant script over
(wantAsset, wantAmount, makerPkScript, optional ratio)and derives the swap VTXO taptree. - Funding. The maker sends the offered funds to the swap address. The
offer's TLV record is embedded in the funding transaction's Arkade
extension packet (type
0x03), making it discoverable from the txid alone. - Discovery. A taker either receives the offer hex out-of-band or reads it from the funding transaction's extension packet.
- Fulfillment. The taker selects inputs from their own wallet, builds a transaction that satisfies the covenant (output 0 / output 1 must pay the maker the wanted amount; for partial fills, the remainder is rebound to the swap address), and submits it through the Emulator and the Operator.
- Settlement. Atomic, in a single Arkade transaction: the maker
receives the wanted amount; the taker receives the offered funds (or
their
fillAmountportion).
If cancelDelay is set, the maker can reclaim the locked funds via the
CLTV cancel leaf after the timelock expires. If exitTimelock is set, the
maker can unilaterally exit on-chain via the CSV exit leaf after a
relative timelock.
The fulfill leaf is an Arkade script embedded in a taproot leaf of the swap VTXO. It uses introspection opcodes to enforce that the fulfillment transaction pays the maker, without requiring a maker signature at spend time.
Verifies two conditions on the fulfillment transaction:
- Value check —
INSPECTOUTPUTVALUE(0) >= wantAmount. - Destination check —
INSPECTOUTPUTSCRIPTPUBKEY(0) == makerPkScript.
For asset swaps (wantAsset set), FINDASSETGROUPBYASSETID +
INSPECTOUTASSETLOOKUP replace the value check to verify that the correct
asset is delivered with the required amount.
When ratioNum and ratioDen are set, the covenant accepts any fill
amount x and computes the price as consumed = x * ratioNum / ratioDen.
The fulfillment transaction MUST:
- Pay the maker
xof the wanted asset at output 1. - If
consumed >= inputAmount— full fill: output 0 carries the entire remaining input value to the maker. - Otherwise — partial fill: output 0 re-creates the swap VTXO with
inputAmount - consumedleft in escrow, preserving the same covenant and scriptPubKey for future takers.
The three combinations (BTC↔asset, asset↔BTC, asset↔asset) each have a
dedicated covenant script; see src/offer.ts.
| Leaf | Spendable by | Condition |
|---|---|---|
| Fulfill | Any taker | Covenant + Emulator cosign + Operator cosign |
| Cancel (optional) | Maker + Operator | CLTV(cancelDelay) reached |
| Exit (optional) | Maker + Operator | CSV(exitTimelock) reached |
Offers are a sequence of TLV records (type: 1B | length: 2B BE | value),
wrapped in an Arkade extension packet of type 0x03.
| Type | Field | Encoding | Required |
|---|---|---|---|
0x01 |
swapPkScript |
raw scriptPubKey | ✓ |
0x02 |
wantAmount |
uint64 BE (sats or asset units) | ✓ |
0x03 |
wantAsset |
serialized AssetId |
for asset wants |
0x04 |
cancelDelay |
uint64 BE (unix timestamp) | optional |
0x05 |
makerPkScript |
raw scriptPubKey (34B) | ✓ |
0x07 |
makerPublicKey |
x-only pubkey (32B) | when cancel or exit is set |
0x08 |
emulatorPubkey |
x-only pubkey (32B) | ✓ |
0x09 |
ratioNum |
uint64 BE | partial fills |
0x0a |
ratioDen |
uint64 BE | partial fills |
0x0b |
offerAsset |
serialized AssetId |
for asset offers |
0x0c |
exitTimelock |
1B type (0=blocks, 1=seconds) + uint64 BE | optional |
Decoders MUST reject unknown TLV types — parsing is strict, not forward-compatible. New types defined in future revisions of this spec will require decoders to be updated before they can process offers that include them.
pnpm add @arkade-os/bancoimport { Maker } from "@arkade-os/banco";
const maker = new Maker(wallet, arkServerUrl, emulatorUrl);
const { offer, swapPkScript, packet } = await maker.createOffer({
wantAmount: 10_000n, // 10k sats
cancelDelay: 86_400, // cancellable after 24h
});
// Fund the swap VTXO. Embedding `packet` as an extension output makes the
// offer discoverable by the funding txid alone.
await wallet.send({
address: ArkAddress.fromPkScript(swapPkScript).encode(),
amount: 0,
assets: [{ assetId, amount: 1000 }],
extensions: [packet],
});
// `offer` (hex) can also be shared out-of-band.const { offer } = await maker.createOffer({
wantAmount: 0n,
wantAsset,
ratioNum: 1_000n, // price: 1000 sats per
ratioDen: 1n, // 1 asset unit
});import { Taker } from "@arkade-os/banco";
const taker = new Taker(wallet, arkServerUrl, emulatorUrl);
// Full fill from a hex offer
const { txid } = await taker.fulfill(offerHex);
// Or read the offer from the funding txid (extension packet on the fly)
const { txid } = await taker.fulfillByTxid(fundingTxid);
// Partial fill
const { txid } = await taker.fulfill(offerHex, { fillAmount: 250n });const arkadeTxid = await maker.cancelOffer(offerHex);Spends the swap VTXO back to the maker via the CLTV cancel leaf. Fails if the offer has no cancel path or if the timelock hasn't expired.
const offers = await maker.getOffers(swapPkScript);
// [{ txid, vout, value, assets, spendable }]| Maker offers | Maker wants | Partial fills |
|---|---|---|
| Asset | BTC | ✓ |
| BTC | Asset | ✓ |
| Asset A | Asset B | ✓ |
git clone --recurse-submodules <this repo> # arkade-regtest is a submodule
pnpm install
pnpm lint
pnpm test # unit tests (excludes test/e2e)
pnpm buildE2E tests run against a local regtest stack: nigiri + arkd (matching ts-sdk's config) + emulator v0.0.1.
pnpm regtest:start # bring up nigiri, arkd, emulator
pnpm test:e2e # run test/e2e/*
pnpm regtest:stop # tear down (preserves volumes)
pnpm regtest:clean # tear down + wipe volumes
pnpm regtest # clean + startregtest/ is the arkade-regtest
submodule. Overrides for the arkd image, fees, and Bitcoin Core config live
in .env.regtest. The emulator is layered on top via
docker-compose.emulator.yml.