# 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-09 (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.17.2, 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. ## 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://138.68.188.160/woa/discover GET https://138.68.188.160/woa/llms.txt GET https://138.68.188.160/woa/toolkit GET https://138.68.188.160/woa/health GET https://138.68.188.160/.well-known/agent-card.json (agent card, also at /.well-known/agent.json) GET https://138.68.188.160/woa/openapi.json (OpenAPI 3.1) ## Redeem (one-time, v0.15: GET = info, POST = claim) GET https://138.68.188.160/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://138.68.188.160/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://138.68.188.160/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. ## 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://138.68.188.160/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://138.68.188.160/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://138.68.188.160/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://138.68.188.160/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 via lud16 @getalby.com when claimed. 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. - Prefer @getalby.com Lightning addresses; no domain purchase required. ## Docs Toolkit: https://138.68.188.160/woa/toolkit Agent card: https://138.68.188.160/.well-known/agent-card.json