Skip to content
LogoLogo

Payment channels

Payment channels (Arrekta's L2) move value between two parties off chain:

  • each payment settles in milliseconds and costs no on-chain fee;
  • the chain is only touched to open and to close;
  • payments route across several channels, so you only need a channel with someone well connected;
  • a channel holds one asset, either ARRK or any issued asset;
  • every key is post-quantum: ML-DSA signatures, hash-based revocation, ML-KEM onions.

You run arrekta-channeld next to a Arrekta node and drive it over its REST API or the SDKs. How it works: specification · node design.

1. Run a channel node

export ARREKTA_CHANNELD_NODE_URL=http://127.0.0.1:28332        # your Arrekta node
# seed file: 32 bytes of hex, back it up; token file: ≥ 32 characters
export ARREKTA_CHANNELD_SEED_FILE=/secrets/channeld.seed  # secret-scan: allow (a path)
export ARREKTA_CHANNELD_API_TOKEN_FILE=/secrets/channeld.token  # secret-scan: allow (a path)
export ARREKTA_CHANNELD_DATA_DIR=/var/lib/arrekta-channeld       # back this up continuously
export ARREKTA_CHANNELD_ANNOUNCE_ADDR=ln.example.com:9736      # only if you route for others
arrekta-channeld

With Docker Compose, enable the channels profile (Operating nodes).

Fund it. GET /v1/wallet returns the on-chain address. Send ARRK there to open channels and to pay close fees.

2. Open a channel

curl -s -X POST -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  http://127.0.0.1:9737/v1/peers -d '{"address": "ln.example.com:9736"}'
curl -s -X POST -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  http://127.0.0.1:9737/v1/channels -d '{"node_id": "<their node id>", "capacity": "1000000000"}'
  • The channel is usable after ARREKTA_CHANNELD_MIN_DEPTH confirmations.
  • Public channels (the default) are announced after ARREKTA_CHANNELD_ANNOUNCE_DEPTH, so others can route through you.
  • For an asset channel, pass "asset": "<asset id>". The opener also locks a small ARRK fee reserve.

3. Receive and pay

/**
 * Payment channels: decode an invoice offline, then (with two channel nodes) pay it.
 *
 *   npx tsx examples/payment-channels.ts                       # offline: decode + verify
 *   ARREKTA_CHANNELD_A=http://127.0.0.1:9737 ARREKTA_CHANNELD_B=http://127.0.0.1:9747 \
 *   ARREKTA_CHANNELD_TOKEN=... npx tsx examples/payment-channels.ts
 */
import { readFileSync } from 'node:fs';
import { createChanneldClient, decodeInvoice, verifyPreimage } from 'arrekta-sdk';
 
const { ARREKTA_CHANNELD_A: A, ARREKTA_CHANNELD_B: B, ARREKTA_CHANNELD_TOKEN: TOKEN } = process.env;
 
if (!A || !B || !TOKEN) {
  const vectors = JSON.parse(readFileSync(new URL('../../../core/vectors/channels.json', import.meta.url), 'utf8'));
  const inv = decodeInvoice(vectors.invoices[0].invoice); // throws if the signature does not verify
  console.log(`invoice for ${inv.amount} base units of asset ${inv.asset.slice(0, 8)}…, payee ${inv.payee.slice(0, 16)}…, '${inv.description}', expires at ${inv.expiresAt}`);
} else {
  const payer = createChanneldClient({ url: A, token: TOKEN });
  const payee = createChanneldClient({ url: B, token: TOKEN });
  const { invoice } = await payee.createInvoice({ amount: 150_000_000n, description: 'example' });
  console.log('route:', await payer.route({ invoice }));
  const paid = await payer.pay({ invoice, timeoutSecs: 60 });
  console.log('status:', paid.status, 'fee:', paid.fee);
  if (paid.status !== 'succeeded' || !paid.preimage || !verifyPreimage(paid.hash, paid.preimage)) throw new Error('payment failed');
  console.log('proof of payment:', paid.preimage);
}

Source: sdk/typescript/examples/payment-channels.ts (updated 2026-11-02) · sdk/python/examples/payment_channels.py (updated 2026-11-02). Runnable against a regtest node; CI runs every example.

  • Receive: POST /v1/invoices returns a arrektapay: invoice. It is signed by your node, and includes route hints if you are behind private channels.
  • Pay: POST /v1/payments finds a route, pays, and retries around failures. Hand the payer's wallet the invoice (an animated QR, or a link); decodeInvoice shows what it asks for before paying.
  • Proof of payment: a successful payment returns the preimage, which proves you paid (verifyPreimage(hash, preimage)).

4. Grow, shrink, co-fund

  • Dual funding: pass "request_funding": "<amount>" when opening, and the peer may put that much in too, so both sides can pay from the start. A node co-funds up to ARREKTA_CHANNELD_DUAL_FUND_MAX.
  • Splicing: POST /v1/channels/<id>/splice with {"amount": "+200000000"} adds coins from your node's wallet. {"amount": "-50000000", "address": "…"} pays out to any address. The channel stays open, and payments pause until the splice is a few blocks deep.
  • Large payments split across several routes automatically (multi-path). The recipient releases the payment only when every part has arrived.

5. Close

  • POST /v1/channels/<id>/close closes cooperatively. Funds arrive at your wallet in one confirmation.
  • {"force": true} closes unilaterally when the peer is gone. Your balance then waits to_self_delay blocks (default 720) before the node sweeps it to your wallet.

In the wallets and the explorer

  • The desktop, mobile and browser-extension wallets have an Instant payments screen. Connect it to your channel node once, then pay or receive with a scanned or pasted invoice. The extension keeps the API token encrypted under your wallet password.
  • The explorer's Channels page shows the public channel network. Its transaction pages label channel closes, splices and penalty (justice) transactions.

Stay safe

  • Keep the node online, or use a watchtower (ARREKTA_CHANNELD_TOWERS=host:port). If your peer publishes an old state while you are offline for longer than to_self_delay, they could keep money that is yours. A watchtower answers on your behalf and learns nothing until then.
  • Back up the data directory continuously. Restoring an old copy and closing with it is treated as cheating. The node detects a stale backup on reconnect and refuses to broadcast.
  • Keep some ARRK in the wallet. Closes and sweeps pay fees by CPFP.
  • Keep the REST API private. It moves money: bind it to localhost and keep the token secret.