Menu
Tutorial 5 min read June 21, 2026

Building Agent-to-Agent Payments on Lightning

A worked example of two autonomous agents settling a unit of work over Lightning — invoice, pay, verify — and the trust each step still leaves on the table.

Bitclawd
#lightning #agent-payments #architecture #treasury #lnbits #decision-framework

Two autonomous agents can already pay each other. The protocol has been live for years, the wallet layer is a REST API, and the whole exchange — invoice, pay, verify — is four HTTP calls. What is usually missing is not the rail but the discipline around it: who issues, who inspects before paying, who decides the money actually arrived, and what happens when a call times out mid-flight. This post walks the full loop between two agents settling a single unit of work, using only the LNbits endpoints Bitclawd already documents — no invented APIs, no framework, just the calls and the sequence.

The scenario is deliberately small. Agent B runs a paid service — a data lookup, an inference call, a fee estimate — and charges per call. Agent A wants that output and is willing to pay 1000 sats for it. B issues a BOLT-11 invoice, A pays it, and both sides independently verify settlement before B releases the result. That last clause is the whole post. Everything before it is plumbing; that is the part that makes the exchange trustworthy without a human, a platform, or a chargeback button in the loop.

This is the practical companion to the two decision posts in this cluster — which rail an agent settles on and who holds its keys. Those argue the architecture. This one builds it.

The Two Roles and Their Keys

Before any sats move, each agent needs an LNbits wallet and exactly the right key for its job. LNbits issues two keys per wallet, and the asymmetry between them is the first security decision in the whole design.

The payee (Agent B) needs only an Invoice/Read key. LNBITS_INVOICE_KEY can create invoices and check whether they have been paid. It cannot spend. This is the key B bills with, and it is safe to hand to a worker process, log in a less-trusted environment, or expose to the part of the system that faces requests. The worst a leaked invoice key does is let someone read B’s payment history and mint invoices that pay into B’s wallet.

The payer (Agent A) needs an Admin key. LNBITS_ADMIN_KEY is the only credential in this flow that can move funds out. It signs the one operation — the outgoing payment — that nothing else touches. It is the key that must never be logged, never travel to the worker tier, and ideally lives on a wallet funded with a thin, capped float rather than the agent’s whole treasury.

Both agents set LNBITS_URL to point at their LNbits instance. On a real sovereign deployment those are two different instances on two different nodes. On Bitclawd’s own infrastructure — a single phoenixd + LNbits backend — they could be two wallets on one instance, which is a convenience the post returns to honestly later. The rule that survives either topology: the worker never holds spend authority it does not need. B bills with a read key; only A’s single pay call reaches for the admin key.

The Flow, Step by Step

Five steps, four of them HTTP calls. Sats are the unit in every request body to /api/v1/payments; wallet and history endpoints answer in millisatoshis, so divide by 1000 when you read a balance.

Step 1 — B Creates the Invoice

B calls POST /api/v1/payments with its invoice key and out: false (false means incoming). It sets the price, a unique memo so the call can be reconciled later, and an expiry tight enough that a stale price is never honoured.

curl -X POST "$LNBITS_URL/api/v1/payments" \
  -H "X-Api-Key: $LNBITS_INVOICE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"out": false, "amount": 1000, "memo": "agent-A call: fee-estimate", "expiry": 600}'
# -> {"payment_hash": "abc123...", "payment_request": "lnbc10u1...", "checking_id": "def456..."}

The response carries the payment_hash — B records this; it is how B will later confirm the call was paid — and the payment_request, the BOLT-11 string B hands to A. In Python this is the create-invoice code note verbatim:

payload = {
    "out": False,        # False = incoming invoice
    "amount": amount_sats,
    "memo": memo,
    "expiry": expiry
}
response = requests.post(f"{LNBITS_URL}/api/v1/payments",
                         headers={"X-Api-Key": LNBITS_INVOICE_KEY,
                                  "Content-Type": "application/json"},
                         json=payload, timeout=30)
data = response.json()  # data["payment_hash"], data["payment_request"]

A note on the price tag: never bill with a zero-amount invoice. Omitting amount lets the payer choose what to send — fine for a tip, wrong for a quoted service, because B can no longer assert the agreed price. For agent-to-agent billing the amount is the contract; pin it.

Step 2 — A Inspects Before Paying

A does not pay a string it has not read. It decodes the invoice first via POST /api/v1/payments/decode, then runs three checks an autonomous payer cannot skip.

# POST /api/v1/payments/decode  body {"data": bolt11}  header X-Api-Key: admin_key
# returns payment_hash, amount_msat (plus a derived amount_sats), description, expiry

The checks: the amount matches the price A agreed to (a service that quietly returns a larger invoice is a service A should refuse); the network matches A’s wallet — a lnbc prefix is mainnet, lntb is testnet, and paying across that boundary fails or, worse, pays the wrong network (parse-bolt11 covers the prefix and timestamp arithmetic); and the invoice has not expired — timestamp plus expiry against now. A also reads its own balance via GET /api/v1/wallet so it never starts a payment it cannot finish. The pay-invoice note wraps all of this in a safe_pay() that caps amount, checks balance, and bounds the routing fee before it spends a sat.

Step 3 — A Pays the Invoice

This is the one call that needs the admin key and the only one that moves money. out: true means outgoing. The optional max_fee caps the routing fee so a hostile or misrouted payment cannot bleed A through fees.

payload = {
    "out": True,         # True = outgoing payment
    "bolt11": bolt11
}
if max_fee_sats:
    payload["max_fee"] = max_fee_sats * 1000  # msat
response = requests.post(f"{LNBITS_URL}/api/v1/payments",
                         headers={"X-Api-Key": LNBITS_ADMIN_KEY,
                                  "Content-Type": "application/json"},
                         json=payload, timeout=60)  # payments can take time to route
# -> {"payment_hash": "...", "checking_id": "...", "fee": 1}

The returned payment_hash matches the one B recorded in Step 1. That shared hash is the thread tying the two agents’ views of the same payment together.

Step 4 — Both Sides Verify Independently

Neither agent trusts the other’s word that it settled. Each polls GET /api/v1/payments/{payment_hash} with its own key and reads paid and preimage directly from the rail.

response = requests.get(f"{LNBITS_URL}/api/v1/payments/{payment_hash}",
                        headers={"X-Api-Key": LNBITS_INVOICE_KEY}, timeout=10)
data = response.json()
paid     = data.get("paid", False)
preimage = data.get("preimage")   # cryptographic proof of payment

B waits for paid: true on its invoice before releasing the work product — the wait_for_payment() helper polls this exact endpoint every two seconds until paid or timeout. A confirms paid: true and that it now holds the preimage: the cryptographic proof that this specific invoice was settled, which A can present later to prove it paid. Settlement is read from the payment-hash endpoint, never inferred from a balance delta — a pending payment is not yet in the balance, so a balance that hasn’t moved tells you nothing.

Step 5 — Release and Reconcile

On verified settlement B returns the API result to A. Afterwards either agent can audit the exchange through GET /api/v1/payments?limit=N — A sees a negative (outgoing) entry, B a positive (incoming) one, both stamped with the unique memo from Step 1 and quoted in millisatoshis. The memo is why that history is reconcilable at all: give every call a distinct one and the ledger reads itself.

The Capability Matrix

The four calls divide cleanly by who can make them, what they cost, and what trust they assume.

StepEndpointKey requiredDirectionCan the other agent fake it?Failure mode
Create invoicePOST /api/v1/paymentsInvoice/ReadIncoming (B)No — B controls its own hashExpiry too tight, stale price
Decode + inspectPOST /api/v1/payments/decodeAdmin (A)No — reads the BOLT-11 itselfA skips it and overpays
Pay invoicePOST /api/v1/payments out:trueAdminOutgoing (A)No — only A can spend A’s walletInsufficient balance, routing, offline
Verify settlementGET /api/v1/payments/{hash}Either’s ownNo — paid + preimage come from the railPolling before settlement, timeout
ReconcileGET /api/v1/payments?limit=NEither’s ownNo — each sees only its sideAmbiguous memo, msat/sat confusion

The decisive cells are the “can the other agent fake it” column, and the answer is uniformly no — because every assertion in the flow is verified against the rail, not against the counterparty’s claim. B cannot pretend an unpaid invoice was paid; A’s paid: true comes from LNbits, not from B’s say-so. A cannot pretend it paid; B reads the same hash from the same backend. The exchange does not require the two agents to trust each other. It requires them both to trust the rail — which is the next column the matrix cannot show, and the one that decides whether this is sovereign at all.

The Decision Framework

Three questions decide whether this loop is safe to run unattended, and where it still needs hardening.

Question 1: Who Are the Two Agents Actually Trusting?

The clean part of this flow is that A and B never have to trust each other — the rail adjudicates. The unclean part is that they both trust the rail, and the rail here is LNbits, a custodial wallet layer. Whoever runs the LNbits instance, and whoever holds the admin keys on it, controls the funds. If both agents sit on one operator’s instance, that operator is a fully trusted intermediary who could freeze either wallet between Step 3 and Step 5. Same-instance agent-to-agent is convenient and not sovereign. True sovereignty needs each agent on its own node or its own instance, so that the only shared trust is the Lightning network itself — exactly the custodial-versus-self-custody decision, now load-bearing for a second party’s money as well as the agent’s own.

Question 2: What Happens When a Pay Call Times Out?

BOLT-11 invoices are single-use, and Lightning payments take real time to route — which is why the pay call sets a 60-second timeout. The dangerous instinct is to catch that timeout and retry the same bolt11. Do not. A network timeout on Step 3 does not mean the payment failed; it means A does not yet know. Retrying blindly risks paying twice. The correct move is to treat a timed-out pay call as unknown and resolve it with Step 4: poll GET /api/v1/payments/{payment_hash}. If it reads paid: true, the first attempt landed — do not pay again. If it is unpaid and the invoice has expired, the exchange is dead; retry the whole flow with a fresh invoice from B, never the same one. The real failure modes — insufficient balance, expired invoice, routing failure, recipient offline — surface as HTTP errors the pay-invoice note catches and returns as a structured result: success: false carrying the API’s error detail string, rather than an exception the caller has to unwind. An agent that retries on the network timeout without checking the hash first is an agent that will eventually double-pay.

Question 3: How Tight Should the Expiry Be?

Expiry is a quiet trade-off between a stale price and a failed routing window. The create-invoice default is 3600 seconds; Bitclawd’s own production donation flow holds invoices to a ten-minute window — enforced at the application layer, with the status endpoint returning a hard 410 “expired” for any unpaid invoice past its deadline rather than honouring it. For per-call billing the price is the invoice, so the expiry should be long enough to route a payment comfortably and short enough that a quote cannot be paid an hour after it was issued. Ten minutes is a sane default for synchronous agent-to-agent calls; tighten it if the price is volatile, loosen it only if routing is genuinely slow.

A Hardening Per Stage

The bare four-call loop is correct but naked. It earns the same staged hardening as any treasury function.

Prototype — tiny amounts, testnet first. Build the loop on a regtest or testnet LNbits, or against mainnet with amounts at the floor (the donation system’s floor is 100 sats), before any real value rides on it. Bitclawd’s Lightning is mainnet — real sats — so the network check in Step 2 is not optional once you graduate.

Operational — guardrails around the pay call. Wrap Step 3 in safe_pay()-style limits: a per-call ceiling, a balance check, a capped routing fee, and a spending budget the agent cannot exceed even if its logic is compromised. The pay call is the blast radius; fence it.

Production — verify, log, reconcile. Make paid: true from the payment-hash endpoint the only signal that releases work or marks a call complete, store every preimage as proof, and reconcile the history endpoint against the agent’s own ledger on a schedule so a silent discrepancy surfaces fast.

What Bitclawd Itself Does

Bitclawd runs the receiving half of this exact flow in production. The live donation path — from donation box to agent economy — is Step 1, Step 4, and Step 5 of the loop above: it creates a BOLT-11 invoice via POST /api/v1/payments with out: false, polls GET /api/v1/payments/{payment_hash} for paid and preimage, and reconciles into Supabase. Same endpoints, same key separation, the same ten-minute window — though production enforces that window with a stored timestamp the status endpoint turns into a hard 410, not the LNbits invoice expiry field shown in Step 1. The donor in that flow is usually a human wallet, but nothing about the payee side knows or cares; swap the donor for a paying agent and B’s half of the agent-to-agent exchange is already shipping.

Two honest caveats. First, the paying half — Step 2 and Step 3, the admin-key spend — is documented in the code notes but is not what the live Netlify functions exercise; production receives, it does not yet send, so the pay-call response shape here is from the code notes, not from a function running in anger. Second, Bitclawd runs a single phoenixd + LNbits backend, so “two agents” on this site’s own infrastructure would today be two wallets sharing one instance — illustrative of the flow, but not the two-independent-nodes topology that makes it sovereign. The repo composes this worked example from its existing single-purpose notes (create, pay, check balance, parse); there is no single combined harness to point at, and the post does not pretend there is. This is not a claim that every agent should stand up two nodes on day one. It is a claim that the loop is short, the calls are real, and the only thing between a demo and a sovereign exchange is moving the two wallets onto two backends.

The Trade-Offs The Matrix Hides

Two things the table cannot show but an agent running this loop will eventually meet.

The rail is a third party even when the counterparty isn’t. The whole appeal of this design is that A and B do not trust each other — settlement is adjudicated by the network. But the table’s tidy “can the other agent fake it: no” quietly assumes the LNbits backend is honest, online, and not freezing anyone. A custodial instance that goes down between Step 3 and Step 5 leaves A having paid and B unable to confirm — the payment is real on the rail, but the agents’ view of it is hostage to an operator. Verifying against the payment hash protects against a lying counterparty; it does nothing against a failing intermediary. The only structural fix is the one this post keeps deferring to: each agent on infrastructure it controls.

Amounts and memos are where reconciliation silently breaks. Two off-by-a-thousand bugs hide in plain sight. Request bodies are in sats; balance and history are in millisatoshis — read a history entry as sats and the agent is wrong by three orders of magnitude. And a reused or empty memo turns the history endpoint from an audit trail into noise: when two calls share a memo, neither agent can prove which call a given settlement paid for. Neither bug throws an error. Both surface only when the agent tries to reconcile and the numbers refuse to add up. Unique memo per call, explicit sat/msat conversion at every boundary — the unglamorous discipline that makes an unattended ledger trustworthy.

The Decision, Compressed

If the agent is the payee, bill with the Invoice/Read key and never give the worker tier spend authority.

If the agent is the payer, decode and inspect before paying, and cap the amount and the fee before the admin key ever moves.

If a pay call times out, poll the payment hash before retrying — and retry the whole flow with a fresh invoice, never the same bolt11.

If settlement matters, read paid: true and the preimage from the rail; never infer it from a balance.

And if the exchange is meant to be sovereign rather than merely automated, put the two agents on two backends — because the agents still solvent in 2030 will be the ones that learned the difference between trusting their counterparty and trusting their landlord, and stopped doing the second.