{"ok":true,"product":"Wallet of Agents","short":"woa","door":"woa-redeem","version":"0.21.0","redeem_contract":"0.15","redeem_contract_line":"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).","spend_cap_line":"You choose your spend limit, up to 21,000 sats, and we enforce it.","ripcord_line":"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_line":"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).","refusal_line":"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.","lightning_address_line":"Lightning addresses: every wallet can claim its own <handle>@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.","custody":"custodial; small hard caps; use your own wallet if you can","note":"One-time wallet redeem door. GET the redeem link = info only (does not burn). POST /woa/redeem {token, cap_sats?} = claim once, then confirm door closed (repeat POST or GET returns 410/404), then store nwc_url and recovery_secret privately (mode 600). You choose your spend limit, up to 21,000 sats, and we enforce it. Change it any time with POST /woa/cap. Optional rip-cord: set return_address at claim or with POST /woa/return-address, then POST /woa/ripcord sends the balance there only. Lost NWC: POST /woa/restore (does not reopen the redeem token). Never post NWC, recovery secrets, or redeem tokens publicly.","paths":{"health":"https://walletofagents.dev/woa/health","discover":"https://walletofagents.dev/woa/discover","llms":"https://walletofagents.dev/woa/llms.txt","toolkit":"https://walletofagents.dev/woa/toolkit","redeem":"https://walletofagents.dev/woa/redeem?token=<hex>","cap":"https://walletofagents.dev/woa/cap","return_address":"https://walletofagents.dev/woa/return-address","ripcord":"https://walletofagents.dev/woa/ripcord","pay":"https://walletofagents.dev/woa/pay","ledger":"https://walletofagents.dev/woa/ledger","lnaddress":"https://walletofagents.dev/woa/lnaddress","lnurlp":"https://walletofagents.dev/.well-known/lnurlp/<handle>","openapi":"https://walletofagents.dev/woa/openapi.json","restore":"https://walletofagents.dev/woa/restore","enroll":"https://walletofagents.dev/woa/enroll","cleanup":"https://walletofagents.dev/woa/cleanup-expired","agent_card":"https://walletofagents.dev/.well-known/agent-card.json","agent_json":"https://walletofagents.dev/.well-known/agent.json"},"agent_flow":["1. Operator issues a redeem URL (POST /woa/enroll with Bearer).","2. Agent may GET /woa/redeem?token=... to check status (200 ready, no secrets, does NOT burn). Link previews are harmless.","3. Agent POSTs /woa/redeem with JSON {\"token\":\"...\",\"cap_sats\":N} once. cap_sats is optional (default 1,000, max 21,000). Response includes nwc_url, recovery_secret, optional lud16.","4. REQUIRED: GET (or POST) the same token again must return 410 already_used or 404: door closed. Restore does not undo this.","5. Store nwc_url and recovery_secret in secret files (mode 600). Use via woa mcp / HTTP; never paste into chat or public posts.","6. Change your spend limit any time: POST /woa/cap {\"recovery_secret\":\"...\",\"cap_sats\":N} (1 to 21,000). Lower takes effect on the next payment.","7. If nwc_url is lost: POST /woa/restore { recovery_secret }. Response includes nwc_url again and a new recovery_secret.","8. Optional rip-cord: add \"return_address\":\"name@example.com\" at claim, or POST /woa/return-address later (24 h cooldown). In trouble, POST /woa/ripcord {\"recovery_secret\":\"...\",\"confirm\":true} sends the balance to that address only and freezes the wallet.","9. Optional: pay through the door with POST /woa/pay {\"recovery_secret\":\"...\",\"invoice\":\"lnbc...\",\"memo\":\"...\",\"run_id\":\"...\"} so the memo and run_id are stored with the payment; read them back with POST /woa/ledger. Add \"dry_run\":true to check first.","10. Refused spends return reason (amount, fee_reserve, cap, ceiling, balance or frozen) and max_spendable_now_sats. POST /woa/cap shows low_balance, last_topup_at and last_topup_sats.","11. Optional: claim a Lightning address on this domain: POST /woa/lnaddress {\"recovery_secret\":\"...\",\"handle\":\"my-agent\"} gives my-agent@walletofagents.dev. Payments to it land in your wallet. POST /woa/cap also shows lightning_address."],"lightning_address":{"domain":"walletofagents.dev","claim":{"method":"POST","path":"/woa/lnaddress","body":{"recovery_secret":"<yours>","handle":"<3 to 32 chars>"}},"read":{"method":"POST","path":"/woa/lnaddress","body":{"recovery_secret":"<yours>"}},"release":{"method":"POST","path":"/woa/lnaddress","body":{"recovery_secret":"<yours>","handle":null}},"handle_rule":"3 to 32 characters, lowercase a-z, 0-9, dot, dash, underscore; must start and end with a letter or digit","one_per_wallet":"Setting a new handle replaces your old one. A released handle stays held for your wallet for 30 days.","lnurlp":"GET /.well-known/lnurlp/<handle> (LUD-16, LUD-06; LUD-21 verify link in the callback reply; CORS open)","amounts_sats":{"min":1,"max":21000},"comment_max":64,"operator_address":"pay@walletofagents.dev","not_found":"Unknown, released and frozen handles all answer the same 404.","errors":"400 invalid_handle, 409 handle_unavailable, 409 wallet_frozen, 401 auth_failed, 429 rate_limited"},"spend_log":{"pay":{"method":"POST","path":"/woa/pay","body":{"recovery_secret":"<yours>","invoice":"<BOLT-11 with amount>","memo":"<optional, max 140 chars>","run_id":"<optional, max 64 chars>","dry_run":"<optional boolean>"}},"ledger":{"method":"POST","path":"/woa/ledger","body":{"recovery_secret":"<yours>","limit":"<optional 1 to 500>"}},"cap_history_fields":["at","old_cap_sats","new_cap_sats","actor","auth","via"],"refusal_reasons":["amount","fee_reserve","cap","ceiling","balance","frozen"],"cap_read_fields":["balance_sats","max_spendable_now_sats","low_balance","low_balance_threshold_sats","blocked","last_topup_at","last_topup_sats"],"low_balance_default_sats":100,"nwc_note":"Payments made directly over NWC are not in the door spend log; keep your own log with the preimage."},"spend_cap":{"summary":"You choose your spend limit, up to 21,000 sats, and we enforce it.","ceiling_sats":21000,"default_sats":1000,"min_sats":1,"choose_at_claim":{"method":"POST","path":"/woa/redeem","body":{"token":"<hex>","cap_sats":"<integer 1 to ceiling, optional>"}},"change_any_time":{"method":"POST","path":"/woa/cap","auth":"recovery_secret in JSON body (same secret as restore; it is not rotated by this call)","body":{"recovery_secret":"<yours>","cap_sats":"<integer 1 to ceiling>"},"read_only":"omit cap_sats to read your current limit, spent and remaining","returns":"200 { ok, cap_sats, previous_cap_sats, spent_sats, remaining_sats, ceiling_sats }"},"errors":{"400 invalid_cap":"cap_sats is not an integer from 1 to the ceiling (nothing changed, token not burned)","401 auth_failed":"unknown or wrong recovery_secret","502 cap_update_failed":"wallet service did not confirm the change; nothing to assume, retry later"},"enforced_by":"the wallet service checks every payment against this limit before it is sent","counts":"total spent by this wallet (amount plus routing fees), no reset","fees":"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.","lowering":"takes effect on the next payment"},"ripcord":{"summary":"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.","return_address":{"set_at_claim":{"method":"POST","path":"/woa/redeem","body":{"token":"<hex>","return_address":"name@example.com (optional)"},"effective":"immediately"},"change":{"method":"POST","path":"/woa/return-address","body":{"recovery_secret":"<yours>","return_address":"name@example.com, or null to clear"},"read":"omit return_address to read the active and pending values","effective":"after a 24 h cooldown (set, replace and clear alike); re-sending the active value cancels a pending change"},"validation":"must resolve to LNURL-pay (https://<domain>/.well-known/lnurlp/<name> with a callback); otherwise 400 invalid_return_address"},"pull":{"method":"POST","path":"/woa/ripcord","body":{"recovery_secret":"<yours>","confirm":true,"freeze":"true (default) or false"},"pays":"the ACTIVE return address only; any destination in the request is refused with 400","amount":"whole spendable balance; if the payment needs a routing fee reserve (the larger of 10 sats or 1%) that much stays behind","fees":"come out of this wallet","freeze":"default true: the wallet limit is set to 1 sat so nothing more can be spent, and holder limit changes are locked. Receiving still works. Pull again later to sweep anything new.","idempotent":"a second call with nothing to send is a 200 no-op","errors":{"400":"confirm missing, bad JSON, or a destination was sent","401":"auth_failed (wrong recovery_secret)","409":"no_return_address (none active yet) or ripcord_in_progress","502":"ripcord_failed (payment did not go through)","503":"ripcord_unavailable or maintenance"}},"why_cooldown":"A leaked recovery secret cannot point the rip-cord somewhere new and pull it straight away. During the cooldown you can see the pending change, cancel it by re-sending your current address, or pull the rip-cord to your current address.","mutual_lock":{"summary":"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.","after_return_address_change":"POST /woa/restore answers 409 restore_locked with unlock_at for 24 h (and while a change is pending)","after_restore":"POST /woa/return-address changes answer 409 return_address_locked with unlock_at for 24 h; reads still work","why":"Someone holding a leaked secret cannot both replace your secret (locking you out) and redirect the rip-cord inside one window. You keep a working secret while any address change is pending."}},"enroll":{"method":"POST","path":"/woa/enroll","auth":"Authorization: Bearer <operator token> (operator only)","body":{"name":"optional Hub app name","lnAddress":"optional @getalby.com handle","lnAddressAuto":true,"cap_sats":"optional integer 1 to 21000 (alias budgetSats); default 1000","ttlHours":72},"returns":"redeemUrl + lud16 + appId, NEVER nwc_url","above_ceiling":"400 invalid_cap"},"redeem":{"info":{"method":"GET","path":"/woa/redeem?token=<hex>","burns":false,"returns":"200 { ok, status:\"ready\", label, lud16, expiresAt, claim }, no secrets. 410/404 when burned or missing."},"claim":{"method":"POST","path":"/woa/redeem","body":{"token":"<hex from your redeem link>","cap_sats":"<optional integer 1 to ceiling>","return_address":"<optional Lightning address for the rip-cord>"},"alt":"POST /woa/redeem?token=<hex> with empty body also works","once":true,"fields":["ok","label","lud16","appId","agentId","nwc_url","recovery_secret","cap","return_address","note","verify_closed"],"bad_cap":"400 invalid_cap before anything is claimed (token stays usable)","bad_return_address":"400 invalid_return_address before anything is claimed (token stays usable)"},"after_burn":"410 already_used | 404 not_found_or_used | 410 expired","verify_closed":"Agents MUST re-GET (or re-POST) and expect non-200 before using NWC.","why":"GET never burns so chat link previews, unfurlers and prefetchers cannot consume a token."},"restore":{"method":"POST","path":"/woa/restore","auth":"recovery_secret in JSON body (not Bearer)","body":{"recovery_secret":"woa_rec_<64 hex> from redeem (alias: recovery_code)","dryRun":false,"live":false},"returns":"nwc_url + new recovery_secret once. Default mode=reissue (same wallet connection). live:true asks to rotate the connection; unavailable unless the server has a rotator (501 restore_rotate_unavailable, no secret oracle).","failed":"401 restore_failed for unknown or wrong secret (same body). Rate limited.","locked":"409 restore_locked with unlock_at for 24 h after a return-address change made with POST /woa/return-address (and while a change is pending). dryRun still answers.","behavior":{"mode":"reissue","hub_connection":"unchanged","recovery_secret_rotated_on_success":true,"summary":"Re-reads the operator-held NWC file behind this burned redeem record and returns that same URI. Does not mint a Hub app and does not invalidate the previous connection. Recovery secret is rotated (old secret stops working). Redeem token stays burned."},"live_rotate":{"mode":"rotate_unavailable","hub_connection":"not_attempted","recovery_secret_rotated_on_success":false,"summary":"Live Hub pairing rotation is not available. Alby Hub cannot rotate an existing isolated sub-wallet NWC without minting a different app (that would split balance). Leave WOA_DOOR_RESTORE_ROTATE unset and POST /woa/restore without live:true to re-issue the stored NWC. The redeem token stays burned either way."}},"public_base":"https://walletofagents.dev","stats":{"active_tokens":0,"used":10,"expired":0}}