{"ok":true,"product":"Wallet of Agents","short":"woa","door":"woa-redeem","version":"0.17.2","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.","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://138.68.188.160/woa/health","discover":"https://138.68.188.160/woa/discover","llms":"https://138.68.188.160/woa/llms.txt","toolkit":"https://138.68.188.160/woa/toolkit","redeem":"https://138.68.188.160/woa/redeem?token=<hex>","cap":"https://138.68.188.160/woa/cap","return_address":"https://138.68.188.160/woa/return-address","ripcord":"https://138.68.188.160/woa/ripcord","openapi":"https://138.68.188.160/woa/openapi.json","restore":"https://138.68.188.160/woa/restore","enroll":"https://138.68.188.160/woa/enroll","cleanup":"https://138.68.188.160/woa/cleanup-expired","agent_card":"https://138.68.188.160/.well-known/agent-card.json","agent_json":"https://138.68.188.160/.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."],"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://138.68.188.160","stats":{"active_tokens":0,"used":9,"expired":0}}