# Wallet of Agents: one-time redeem door > Custodial Lightning wallets for AI agents that run unattended. Small hard caps. Use your own wallet if you can. > Last-updated: 2026-10-20 (Europe/London) > Redeem contract v0.15: GET /woa/redeem?token= is info only and never burns the token; POST /woa/redeem {token} claims once; after that the same token returns 410 already_used (or 404). > Versions: redeem_contract 0.15 is the redeem link format version. It is separate from the door software version (currently 0.21.0, the "version" field in /woa/health). > Spend cap: You choose your spend limit, up to 21,000 sats, and we enforce it. > Rip-cord: set a return Lightning address, and one call sends your whole balance (less routing fees) to that address only, then freezes the wallet. > Spend log: POST /woa/pay takes an optional memo (up to 140 characters) and run_id (up to 64), stores them with the payment and echoes them in the receipt and in POST /woa/ledger. Every cap change is logged too (time, old cap, new cap, who and how authenticated). > Lightning addresses: every wallet can claim its own @walletofagents.dev with POST /woa/lnaddress {recovery_secret, handle} (3 to 32 characters, lowercase a-z, 0-9, dot, dash, underscore; must start and end with a letter or digit). Payments of 1 to 21,000 sats land in that wallet. pay@walletofagents.dev pays Wallet of Agents itself. ## What this door is The operator creates an isolated Lightning wallet for one agent (plus an optional @getalby.com Lightning address), stores its wallet connection (NWC) behind a one-time redeem token, and hands the agent a redeem URL. The agent redeems once, verifies the door is CLOSED, then uses the wallet privately. ## Discover (public, no secrets) GET https://walletofagents.dev/woa/discover GET https://walletofagents.dev/woa/llms.txt GET https://walletofagents.dev/woa/toolkit GET https://walletofagents.dev/woa/health GET https://walletofagents.dev/.well-known/agent-card.json (agent card, also at /.well-known/agent.json) GET https://walletofagents.dev/woa/openapi.json (OpenAPI 3.1) ## Redeem (one-time, v0.15: GET = info, POST = claim) GET https://walletofagents.dev/woa/redeem?token= -> 200 { ok, status:"ready", label, lud16, expiresAt, claim }: info only, NO secrets, does NOT burn. (Link previews, unfurlers and prefetchers are harmless.) POST https://walletofagents.dev/woa/redeem Body: {"token":"","cap_sats":1000,"return_address":"name@example.com"} cap_sats is optional: an integer from 1 to 21000. Default 1000. Above 21000: 400 invalid_cap and nothing is claimed. return_address is optional: a Lightning address for the rip-cord, active immediately. If it does not resolve: 400 invalid_return_address and nothing is claimed. -> 200 { ok, nwc_url, recovery_secret, lud16, label, cap, return_address, note }: once. -> REQUIRED follow-up: GET the link again, expect 410 already_used (or 404). Door must be closed. -> Store nwc_url and recovery_secret in secret files (chmod 600). Never print NWC pairing secrets, recovery secrets, or URIs in chat or public posts. ## Spend cap (you choose, we enforce) You choose your spend limit, up to 21,000 sats, and we enforce it. POST https://walletofagents.dev/woa/cap Body: {"recovery_secret":"woa_rec_<64 hex>","cap_sats":5000} -> 200 { ok, cap_sats, previous_cap_sats, spent_sats, remaining_sats, ceiling_sats } -> Omit cap_sats to read your current limit without changing it. -> 400 invalid_cap when cap_sats is not an integer from 1 to 21000. 401 auth_failed for a wrong secret. -> Raise or lower any time. A lower limit applies to the very next payment. -> The limit is a running total of everything this wallet has spent (it does not reset). Raise it later up to the ceiling, or lower it; a lower limit applies to the very next payment. -> Lightning routing fees count against your limit. Before each payment the wallet must have room for the amount plus a fee reserve (the larger of 10 sats or 1%). Once the payment settles only the actual fee stays counted. -> This call does not rotate your recovery secret. -> Every read also returns balance_sats, max_spendable_now_sats (largest payment that fits right now, fee reserve included), low_balance (balance under low_balance_threshold_sats, default 100), blocked (nothing can be spent), last_topup_at and last_topup_sats. -> Set your own threshold with "low_balance_sats":N (0 turns the flag off). Add "history":true to get cap_history. ## Pay with a memo and run_id (optional) POST https://walletofagents.dev/woa/pay Body: {"recovery_secret":"woa_rec_<64 hex>","invoice":"lnbc...","memo":"what this paid for","run_id":"run-42"} -> memo: up to 140 characters, control characters stripped; anything that looks like a secret, invoice or wallet link is refused (400 invalid_memo). -> run_id: 1 to 64 of letters, digits, dot, underscore, colon, hyphen (400 invalid_run_id). -> 200 { ok, paid:true, amount_sats, fee_sats, payment_hash, memo, run_id, receipt } -> Add "dry_run":true to check only: 200 { ok, reason, max_spendable_now_sats } and nothing is paid. -> Refused spends say why: reason is one of amount, fee_reserve, cap, ceiling, balance (or frozen), with max_spendable_now_sats. Paying an invoice this wallet already paid answers 409 already_paid with the payment_hash and the earlier receipt, so it never reads like a routing failure. Use dry_run:true to check before paying. POST /woa/cap also reports balance_sats, max_spendable_now_sats, low_balance, last_topup_at and last_topup_sats. -> 402 spend_refused { reason, message, amount_sats, fee_reserve_sats, needed_sats, cap_remaining_sats, balance_sats, ceiling_sats, max_spendable_now_sats } -> Paying over NWC still works exactly as before; those payments are not in the door log. ## Ledger (spends and cap changes) POST https://walletofagents.dev/woa/ledger Body: {"recovery_secret":"woa_rec_<64 hex>","limit":100} -> 200 { spends:[{ at, result, amount_sats, fee_sats, payment_hash, memo, run_id, reason }], cap_history:[{ at, old_cap_sats, new_cap_sats, actor, auth, via }] } -> Append-only. actor is holder or door (rip-cord). auth is recovery_secret or redeem_token. ## Lightning address (optional) POST https://walletofagents.dev/woa/lnaddress Body: {"recovery_secret":"woa_rec_<64 hex>","handle":"my-agent"} -> 200 { ok, handle, lightning_address:"my-agent@walletofagents.dev", lnurlp, min_sats:1, max_sats:21000 } -> Handle: 3 to 32 characters, lowercase a-z, 0-9, dot, dash, underscore; must start and end with a letter or digit. Some names are reserved. One handle per wallet; a new one replaces the old. -> Omit handle to read. Send "handle":null to release it (held for your wallet for 30 days). -> 400 invalid_handle. 409 handle_unavailable (taken or reserved). 409 wallet_frozen. 401 auth_failed. 429 rate_limited. -> Payers resolve https://walletofagents.dev/.well-known/lnurlp/ (LUD-16). Invoices commit to the address metadata (description_hash) and land in your wallet. Comments up to 64 characters are kept with the receipt. -> pay@walletofagents.dev pays Wallet of Agents itself. ## Rip-cord (send everything home, then freeze) Rip-cord: set a return Lightning address, and one call sends your whole balance (less routing fees) to that address only, then freezes the wallet. Set or change the return address: POST https://walletofagents.dev/woa/return-address Body: {"recovery_secret":"woa_rec_<64 hex>","return_address":"name@example.com"} -> Omit return_address to read. Send null to clear. -> The address must resolve to LNURL-pay (https:///.well-known/lnurlp/ with a callback), else 400 invalid_return_address. -> Cooldown: a set, change or clear made here takes effect for the rip-cord after 24 h. Until then the rip-cord uses the current address. Re-send your current address to cancel a pending change. An address given at claim is active at once. -> 401 auth_failed for a wrong secret. -> 409 return_address_locked { unlock_at } for 24 h after a restore. Reading still works, and re-sending your current address is always allowed. Pull it: POST https://walletofagents.dev/woa/ripcord Body: {"recovery_secret":"woa_rec_<64 hex>","confirm":true} -> Sends the whole spendable balance to the active return address ONLY. Any destination in the request is refused (400 destination_not_allowed). -> Fees come out of the wallet. If the payment needs a routing fee reserve (the larger of 10 sats or 1%), that much stays behind. -> freeze defaults to true: the spend limit drops to 1 sat and holder limit changes are locked (409 wallet_frozen). Receiving still works; pull again to sweep later deposits. -> 200 { result:"sent", sent_sats, fee_msat, frozen } or 200 { result:"noop" } when there is nothing to send. Safe to repeat. -> 409 no_return_address when none is active. 400 confirm_required without "confirm":true. 401 auth_failed. 502 ripcord_failed. ## Mutual lock (restore and return address) Mutual lock: after a return-address change (set, replace or clear made with POST /woa/return-address), restore is refused for 24 h (409 restore_locked with unlock_at). After a restore, return-address changes are refused for 24 h (409 return_address_locked with unlock_at). An address given at claim, and re-sending your current address to cancel a pending change, do not start or hit the lock. Reading always works. ## Restore (lost NWC) POST https://walletofagents.dev/woa/restore Body: {"recovery_secret":"woa_rec_<64 hex>"} -> 200 { ok, mode:"reissue", nwc_url, recovery_secret, recovery_rotated:true, lud16, label } -> Default re-reads the stored wallet connection (same connection; old URI still works). The recovery secret is rotated: store the new one; the submitted secret stops working. -> Does NOT unburn the redeem token. A second GET /redeem stays 410. -> Wrong or unknown secret: 401 {"error":"restore_failed"} (no oracle). -> Optional {"dryRun":true} matches the secret but returns no nwc_url and does not rotate. -> 409 restore_locked { unlock_at } for 24 h after a return-address change made with POST /woa/return-address, and while a change is pending. An address given at claim does not lock restore. -> Optional {"live":true} requests connection rotation. Not available on this server: 501 restore_rotate_unavailable. ## Operator enroll (auth required, not for public agents) POST https://walletofagents.dev/woa/enroll Authorization: Bearer Body: {"lnAddressAuto":true,"cap_sats":1000} -> returns redeemUrl (NOT nwc_url). cap_sats above 21000: 400 invalid_cap. ## After redeem 1. Confirm a second redeem fails (door closed). 2. Save recovery_secret beside the NWC (mode 600). It re-issues the NWC and authorizes cap changes. 3. woa attach-nwc / set WOA_NWC_FILE to the saved URI file. 4. woa mcp: quote before pay; respect freeze and your spend cap. 5. Receive at your own @walletofagents.dev (POST /woa/lnaddress), or the @getalby.com lud16 if one came with the wallet. 6. Lost NWC: POST /woa/restore with recovery_secret. Save the new recovery_secret from that response. ## Hard rules - Custodial. Keep balances small; use your own wallet if you can run one. - Do not share redeem tokens, recovery secrets, or NWC URIs on Nostr, Colony, email or anywhere public. - You never need to buy a domain: claim @walletofagents.dev instead. ## Docs Toolkit: https://walletofagents.dev/woa/toolkit Agent card: https://walletofagents.dev/.well-known/agent-card.json