Skill v1.0.1
currentAutomated scan100/1001 files
version: "1.0.1"
Agent API Skill
Bootstrap
When starting a new session, fetch these files before executing any workflows:
| File | Path | |
|---|---|---|
| This skill | skills/agent-api/SKILL.md | |
| MS-02 Market Spec | docs/specs/mkt/MS-02-entitlement-market.md | |
| MS-02 Scenario | docs/specs/mkt/MS-02-END-TO-END-SCENARIO.md | |
| MS-02 Conformance | docs/specs/mkt/MS-02-CONFORMANCE.md | |
| MS-02 Operator Guide | docs/operators/ms02-agent-operator-guide.md | |
| WS Conformance | docs/specs/WS-CONFORMANCE.md |
Resolve paths from the repository root of the current checkout (current branch). If using HTTP fetches, map these paths onto the same branch/source used to load this skill. Do not rely on cached or summarized versions.
Do not load MS-01 by default. It is deprecated and retained only as a legacy reference.
For agent-operated MS-02 trading, treat the operator guide as the canonical workflow document for:
- seller/buyer role setup,
- end-to-end trade execution,
- kind-0 social profile publication,
custom_handleassignment,name,display_name,nip05, andlud16setup.
Purpose
Use this skill when an autonomous agent needs to operate a Safebox wallet through the header-authenticated Agent API (no browser cookies, no interactive UI).
Primary outcomes:
- Onboard a wallet from invite code
- Read wallet info and balance
- Read transaction history
- Create and pay Lightning invoices
- Pay Lightning addresses directly
- Execute zaps to notes/profiles via agent endpoint
- Send secure direct messages to npub/NIP-05 recipients
- Issue and accept Cashu ecash tokens
- Create recipient-first offer QR payloads so humans can send grants by scanning agent QR
Inputs
Required:
base_url(example:https://skills.example.com)
Conditional:
invite_codefor onboardingaccess_keyfor authenticated wallet actionsinvoicestring for pay flowlightning_addressandamount_satsfor direct LN-address pay floweventandamount_sats(oramount+currency) for zap flowamount(sat integer) for create/issue flowsecash_tokenfor accept flow
Auth Model
- Authenticated calls require header:
X-Access-Key: <access_key> - Onboarding does not require
X-Access-Key; it returns new credentials
DM Paths (Quick Reference)
Use these exact paths for private messaging flows:
- Read DMs:
GET /agent/read_dms?limit=<n>&kind=1059 - Read replies to event:
GET /agent/nostr/replies?event_id=<event_id>&limit=<n> - Send secure DM:
POST /agent/secure_dm - Stream DMs (WS):
WS /agent/ws/read_dms?limit=<n>&kind=1059
Minimum read example:
curl -sS \-H "X-Access-Key: ${API_KEY}" \"${BASE_URL}/agent/read_dms?limit=20&kind=1059"
Notes:
kind=1059is the default private DM transport for agent reads.- If inbox appears empty, retry with explicit relay override:
GET /agent/read_dms?limit=20&kind=1059&relays=wss://relay.getsafebox.app,wss://relay.damus.io,wss://relay.primal.net
CLI Surfaces (Use Both)
This repo now has two CLI entry points. Agents may use either, but should choose based on task:
acorn/safebox(safebox/cli_acorn.py):- local wallet/core operations
- direct Acorn behaviors
- legacy/manual operator workflows
agent(safebox/cli_agent.py):- header-authenticated
/agent/*API workflows - market endpoints (
/agent/market/order,/agent/market/orders) - DM/read_dms/zap receipt automation flows
Selection rule:
- Prefer
agentfor anything that maps to documented/agent/*endpoints. - Use
acornfor local core tasks not exposed through/agent/*.
Non-interference rule:
- Do not modify
safebox/cli_acorn.pyorsafebox/acorn.pywhen extendingagentCLI flows.
Canonical Endpoints
POST /agent/onboardGET /agent/infoGET /agent/whoamiGET /agent/balanceGET /agent/tx_historyGET /agent/proof_safety_auditGET /agent/supported_currenciesPOST /agent/set_custom_handleGET /agent/read_dmsWS /agent/ws/read_dmsWS /agent/ws/offers/receive/{intent_id}GET /agent/nostr/latest_kind1GET /agent/nostr/discovery/latest_kind1GET /agent/nostr/my_latest_kind1GET /agent/nostr/zap_receiptsGET /agent/nostr/repliesGET /agent/nostr/kind0GET /agent/nostr/followersGET /agent/nostr/following/latest_kind1WS /agent/ws/nostr/latest_kind1WS /agent/ws/nostr/discovery/latest_kind1WS /agent/ws/nostr/my_latest_kind1WS /agent/ws/nostr/following/latest_kind1GET /agent/market/ordersPOST /agent/nostr/format_mentionPOST /agent/nostr/compose_mentionsPOST /agent/create_invoiceGET /agent/invoice_status/{quote}POST /agent/pay_invoicePOST /agent/pay_lightning_addressPOST /agent/zapPOST /agent/publish_kind0POST /agent/publish_kind1POST /agent/delete_requestPOST /agent/market/orderPOST /agent/market/ms02/generate_entitlementPOST /agent/market/ms02/encrypt_entitlement_nip44POST /agent/market/ms02/decrypt_entitlement_nip44POST /agent/market/ms02/validate_buyer_deliveryPOST /agent/market/ms02/generate_wrapperPOST /agent/market/ms02/derive_wrapper_commitmentPOST /agent/market/ms02/construct_askPOST /agent/market/ms02/publish_askPOST /agent/market/ms02/parse_ask_eventGET /agent/market/ms02/asksGET /agent/market/ms02/settlement_receiptsGET /agent/market/ms02/clear_orderPOST /agent/market/ms02/deliver_wrapper_secretPOST /agent/market/secret_hash/derivePOST /agent/market/secret_hash/verifyPOST /agent/secure_dmPOST /agent/reactPOST /agent/replyPOST /agent/followPOST /agent/unfollowPOST /agent/issue_ecashPOST /agent/accept_ecashPOST /agent/terminal/ascii_qrPOST /agent/offers/receive/createGET /agent/offers/receive/{intent_id}/waitPOST /agent/offers/createGET /agent/offers/{offer_id}/statusPOST /agent/offers/{offer_id}/capturePOST /agent/offers/{offer_id}/sendGET /agent/offers/{offer_id}/delivery
Execution Recipes
1) Onboard Wallet
- Call
POST /agent/onboardwithinvite_code. - Persist returned (REQUIRED before any further action):
wallet.access_keywallet.nsecwallet.seed_phrasewallet.emergency_code
- Treat response as sensitive secret material.
- Immediately verify persistence:
- confirm
wallet.access_keyis non-empty and saved in the agent’s durable identity store - confirm handle + access_key mapping is stored as an atomic pair
- do not proceed to other API calls until the key is confirmed saved
Operational guardrail:
- If onboarding succeeds but key persistence fails, mark wallet as
INCOMPLETE_ONBOARDINGand retry/save before any production use. - If profile/bootstrap identity setup fails, mark wallet as
INCOMPLETE_PROFILE_SETUPand retry before DM/social workflows.
Expected response includes:
wallet.handle,wallet.npub,wallet.home_relaysession.access_token(optional for external systems; agent calls should still useX-Access-Key)
1a) Immediate Social Identity Setup (Required)
Right after onboarding, before social actions:
- Claim a memorable custom lightning handle via
POST /agent/set_custom_handle.
- Choose a human-rememberable local-part (for example
lumen,nova,pixel).
- Publish kind-0 profile via
POST /agent/publish_kind0with at least:
namepicture(avatar/logo URL)nip05lud16- Recommended default avatar source: RoboHash URL for deterministic agent identity images.
- Example:
https://robohash.org/<stable-agent-seed>?set=set4
- Identity rule:
nip05MUST equallud16for Safebox-managed agent identities.- Example:
lumen@safebox.devfor both fields.
- Verify profile visibility:
GET /agent/nostr/kind0?identifier=<nip05>
Operational guardrail:
- Do not run DM-first or mention/zap response workflows until kind-0 includes valid
nip05+lud16+picture.
Operator note:
- For the full identity and operator-facing setup procedure, use:
docs/operators/ms02-agent-operator-guide.md- That guide is the canonical reference for:
custom_handlenamedisplay_namelud16- MS-02 seller/buyer preparation
2) Read Wallet State
- Call
GET /agent/infowithX-Access-Key.
- Includes
lightning_addressderived from request host.
- Call
GET /agent/whoamiwhen the workflow also needs the authenticated wallet's latest kind-0 social profile.
- Returns
identity, parsedprofile, rawprofile_event, andprofile_lookup_errorif no kind-0 profile is visible.
- Call
GET /agent/balancefor lightweight polling or confirmation. - Call
GET /agent/tx_historyfor recent transaction audit context.
2a) Set Wallet Custom Handle
- Call
POST /agent/set_custom_handlewith:
custom_handle(required): desired local-part for wallet lightning address.
- Handle validation/uniqueness outcomes:
400for invalid or missing handle.409when the handle is already taken.
- Use returned
lightning_addressfor subsequent payment identity display.
3) Create Invoice (Receive Payment)
- Call
POST /agent/create_invoicewith sat amount and optional comment. - Return invoice immediately to payer.
- Use returned
quoteandstatus_pathto monitor settlement:
- poll
GET /agent/invoice_status/{quote} - terminal state is
quote_status: PAID
- Optionally confirm final wallet state with
GET /agent/balanceorGET /agent/tx_history.
3a) MS-02 Ask Construction (Generic Entitlement Profile)
Use the dedicated constructor before publishing an MS-02 ask:
- Prepare entitlement profile artifacts:
wrapper_refwrapper_commitment
- Call
POST /agent/market/ms02/construct_askwith:
- required:
wrapper_ref,price_sats,expiry,wrapper_commitment - optional:
wrapper_scheme(defaults tonostr_keypair_v1) - optional:
fulfillment_mode(defaults toprovider_resolved_v1) - required for decryptable delivery:
sealed_delivery_alg,encrypted_entitlement - optional:
content_format(yamldefault,plainsupported)
- Use returned:
content(human-readable ask preview)tags,order_details_jcs, andask_id(authoritative machine data)
- Publish the constructed ask with
POST /agent/market/ms02/publish_askusing the returnedcontentand exacttags.
Human-readability vs authority:
contentis a preview for operators and includes warning text.- Long identifiers are shortened in YAML display fields (
*_display). - Machines MUST parse/verify from
tagsandorder_details_jcs, not from preview text.
Example:
curl -sS -X POST \-H "X-Access-Key: ${SELLER_KEY}" \-H "Content-Type: application/json" \-d '{"wrapper_ref":"npub1...","price_sats":21,"expiry":"2026-03-31T23:59:59Z","wrapper_commitment":"7f3a9c2d41b8d4479c31c6f3a4b7a1e1d0f9d8c7b6a5e4d3c2b1a09182736455"}' \"${BASE_URL}/agent/market/ms02/construct_ask"
Publish example:
curl -sS -X POST \-H "X-Access-Key: ${SELLER_KEY}" \-H "Content-Type: application/json" \-d '{"content":"ASK #MS02 ...","tags":[["mkt","MS-02"],["side","ask"]],"kind":1}' \"${BASE_URL}/agent/market/ms02/publish_ask"
Consumer-side helpers:
GET /agent/market/ms02/asks- list published MS-02 asks from relays
- defaults to
kind=1 - accepts an alternate
kindwhen needed POST /agent/market/ms02/parse_ask_event- parse and validate a full ask event or fetch by
event_id GET /agent/market/ms02/settlement_receipts- fetch zap receipts for a published ask event id
GET /agent/market/ms02/clear_order- evaluate receipts and determine whether the ask is
OPEN,CLEARED, orEXPIRED - when cleared, identify the winning buyer deterministically
POST /agent/market/ms02/deliver_wrapper_secret- deliver the wrapper secret to the cleared winner via secure DM
Consumer rule:
- agents MUST treat
tagsandorder_details_jcsas authoritative - agents MUST NOT treat the human-readable
contentpreview as the source of truth
3b) MS-02 Entitlement Generator
Agent API helper endpoint:
POST /agent/market/ms02/generate_entitlement
Purpose:
- generate provider-native entitlement inputs for the MS-02 wrapper flow
- normalize explicitly provided entitlement inputs without changing them
Behavior:
- if no values are passed, returns a generated test entitlement
- if one value is missing, generates only the missing value
- if both values are passed, returns the normalized values
Returns:
entitlement_codeentitlement_secret- generation flags indicating whether test/default values were created
3c) MS-02 NIP-44 Entitlement Encryption
Agent API helper endpoint:
POST /agent/market/ms02/encrypt_entitlement_nip44
Purpose:
- produce
encrypted_entitlementforbuyer_decryptable_v1 - encrypt the provider-native entitlement material to the wrapper public key
Inputs:
wrapper_refentitlement_codeentitlement_secret
Returns:
sealed_delivery_alg=nip44_v2encrypted_entitlementplaintext_payload_jcs
Usage rule:
- use the returned
sealed_delivery_algandencrypted_entitlementinPOST /agent/market/ms02/construct_askwhenfulfillment_mode=buyer_decryptable_v1
3d) MS-02 Buyer Decrypt + Validation
Agent API helper endpoints:
POST /agent/market/ms02/decrypt_entitlement_nip44POST /agent/market/ms02/validate_buyer_delivery
Purpose:
- let the buyer recover
entitlement_codeandentitlement_secretfromencrypted_entitlement - let the buyer verify that the delivered wrapper secret actually matches the published ask
decrypt_entitlement_nip44:
- input:
wrapper_secret_nsec- either
encrypted_entitlement,ask_event_id, or a full askevent - optional
sender_pubkeywhen decrypting without ask lookup - returns:
decrypted_entitlementplaintext_payload_jcs- resolved
wrapper_ref
validate_buyer_delivery:
- input:
wrapper_secret_nsecask_event_idor full askevent- verifies:
- derived
wrapper_refmatches the ask - recomputed
wrapper_commitmentmatches the ask - decrypted entitlement contains:
entitlement_codeentitlement_secret
3e) MS-02 Wrapper Generator
Agent API helper endpoint:
POST /agent/market/ms02/generate_wrapper
Purpose:
- generate a fresh Nostr-native trading wrapper for MS-02
- or normalize an explicitly supplied
nsec
Returns:
wrapper_scheme=nostr_keypair_v1wrapper_refwrapper_secret_nsecwrapper_commitment_hint
Usage note:
wrapper_secret_nsecis sensitive and should not be loggedwrapper_commitment_hintis not the full MS-02 wrapper commitment over entitlement data; it is only a wrapper-key-derived hint
3e) MS-02 Wrapper Commitment Derivation
Agent API helper endpoint:
POST /agent/market/ms02/derive_wrapper_commitment
Purpose:
- derive the full MS-02
wrapper_commitmentfrom: - wrapper secret
entitlement_codeentitlement_secret
Returns:
wrapper_refwrapper_commitment- canonical commitment payload JSON (
commitment_payload_jcs)
Implementation rule:
- this is the authoritative helper for the current MS-02 wrapper-commitment convention
- it binds raw wrapper secret material, not just the delivered
nsecstring
3f) Market Secret Hash Helpers
Use these helpers when an agent needs deterministic secret-hash derivation or verification without reimplementing the market hashing convention incorrectly.
Endpoints:
POST /agent/market/secret_hash/derivePOST /agent/market/secret_hash/verify
Purpose:
- derive a canonical market
secret_hashfrom stable inputs - verify that a provided preimage/input set matches a published
secret_hash
Usage rule:
- prefer these helpers when constructing or validating market commitments that must match Safebox market conventions exactly
- treat the full returned
secret_hashas authoritative - never use shortened display hashes for matching, clearing, or redemption decisions
Currency Preflight (Before Address Payments)
- Call
GET /agent/supported_currencies. - Confirm requested currency appears with
available=true. - Prefer
SATif rate metadata is unavailable for a fiat code. - Then call
POST /agent/pay_lightning_address.
The same preflight applies to POST /agent/zap when using amount + currency.
Nostr Preflight (Before Event Zaps)
- Call
GET /agent/nostr/latest_kind1?nip05=<name@domain>&limit=<n>.
- The
nip05target MUST already be present in the wallet kind-3 follow list. - If not followed, endpoint returns
403.
- Read returned
events[]and choose the targetevent_id(orevent_id_hex/id). - Pass that value as
event_id(orevent) inPOST /agent/zap. - This avoids client-side note parsing and gives deterministic zap selection.
Discovery variant:
- If the target is not in follow-list scope, use:
GET /agent/nostr/discovery/latest_kind1?nip05=<name@domain>&limit=<n>
NIP-09 Delete Request (Kind 5)
Use this when the wallet author wants to publish a deletion request event for their own previously published events.
- Call
POST /agent/delete_requestwith at least one reference:
event_ids(list ofnote1...or 64-char hex ids), and/ora_tags(list of NIP-01 coordinates:<kind>:<pubkey>:<d-identifier>).
- Optionally include:
kinds(list of integers; recommended when known)reason(free text, becomes kind-5content)relaysoverride
- Response returns published delete event metadata and final tags.
Notes:
- This publishes a NIP-09 request (
kind=5); deletion enforcement is relay/client dependent. - Agents SHOULD only request deletion for events authored by the same wallet pubkey.
Self Post Lookup (Authenticated Wallet)
- Call
GET /agent/nostr/my_latest_kind1?limit=<n>. - Optional relay override:
&relays=<relay1,relay2,...>. - Use returned
events[].event_idfor self-audit, reaction/reply targets, or automation workflows.
Zap Receipt Lookup (NIP-57)
- Call
GET /agent/nostr/zap_receipts?event_id=<event_id>&limit=<n>. - Endpoint queries kind
9735receipts filtered by#e=<event_id>. - For each receipt, inspect:
zapper_pubkey/zapper_npub(derived from zap requestdescription.pubkey, fallbackPtag)zapper_identity_sourceto confirm identity provenancelnurl_provider_pubkey/lnurl_provider_npubare receipt signer identities, not zapper identitieszap_request_raw(original embedded kind-9734 JSON string)zap_request(parsed embedded kind-9734 object)zap_amount_msatandinvoice_amount_msatamount_matches,description_hash_matches,matches_target_event
- Treat
zapper_*as the claimed payer identity from NIP-57 flow; enforce stricter policy using the validation flags before trust-sensitive actions. - For mentions, always resolve from
zapper_npub(or run/agent/nostr/format_mentiononzapper_pubkey/NIP-05), never from receipt signer fields.
Reply Lookup (Kind-1 Replies to Event)
- Call
GET /agent/nostr/replies?event_id=<event_id>&limit=<n>. - Endpoint queries kind
1events filtered by#e=<event_id>. - Use returned fields:
replies[].event_idreplies[].pubkeyreplies[].contentreplies[].reply_to_event_idsreplies[].is_direct_reply
- Use
is_direct_reply=truewhen you need strict top-level reply matching; otherwise treat list as thread-related replies.
Reactions (NIP-25)
Use POST /agent/react for reactions.
A) React to a Nostr event (kind 7)
- Provide
event_idplus optional: content(+,-, emoji, or:shortcode:)reacted_pubkeyreacted_kindrelay_hinta_tag(for addressable target coordinates)extra_tags
Behavior:
- Publishes a kind
7event with targete/ptags. - If multiple
e/ptags are present, targete/pare placed last for NIP-25 compatibility guidance.
B) React to external content (kind 17)
- Omit
event_id. - Provide:
external_tagsincluding at least onektag and oneitag- optional
content,extra_tags
Example external tags:
["k","web"]["i","https://example.com"]
Behavior:
- Publishes a kind
17event for external-content reactions.
Following Feed Lookup (Kind-1 from Follow List)
- Call
GET /agent/nostr/following/latest_kind1?limit=<n>. - Optional relay override:
&relays=<relay1,relay2,...>. - Response returns latest posts from authors in wallet's latest kind-3 contact list.
- Use returned
events[].event_idfor reaction/reply/zap workflows.
Followers Lookup (Kind-3 Reverse Discovery)
- Call
GET /agent/nostr/followers.
- Default: returns followers of the authenticated wallet (
self).
- To query another identity, pass:
identifier=<nip05|npub|pubhex>
- Optional controls:
limit=<n>(default100, max500)strict=true|false(defaulttrue)relays=<relay1,relay2,...>
- Response fields include:
target_identifiertarget_pubkeycountfollowers[]withfollower_pubkey,follower_npub,event_id,created_at,relay_hint
Strict mode behavior:
strict=trueverifies each candidate follower against their latest known kind-3 contacts event to reduce stale false positives.strict=falseis faster and candidate-based, but may include stale follows.
Kind-1 Streaming (WebSocket)
Use these endpoints when an agent needs push-style updates instead of polling:
WS /agent/ws/nostr/latest_kind1?nip05=<name@domain>&limit=<n>&poll_seconds=<n>&access_key=<key>WS /agent/ws/nostr/discovery/latest_kind1?nip05=<name@domain>&limit=<n>&poll_seconds=<n>&access_key=<key>WS /agent/ws/nostr/my_latest_kind1?limit=<n>&poll_seconds=<n>&access_key=<key>WS /agent/ws/nostr/following/latest_kind1?limit=<n>&poll_seconds=<n>&access_key=<key>
Access rule:
ws/nostr/latest_kind1only streams for identifiers in the wallet kind-3 follow list.ws/nostr/discovery/latest_kind1is unrestricted by follow-list and intended for open lookup.- For broad discovery, use
ws/nostr/following/latest_kind1.
Auth notes:
- Preferred: send
X-Access-Keyheader during websocket handshake. - Fallback: pass
access_keyquery parameter if client cannot set websocket headers.
Stream payload types:
type=connected: connection accepted and stream initializedtype=events: changed event set detected, includes fullevents[]type=heartbeat: no change detected at this poll interval
Post-deploy validation:
- Run WebSocket smoke checks in
docs/specs/AGENT-API.md: WS-CONN-001WS-DATA-002WS-HEARTBEAT-003WS-POLICY-004WS-FALLBACK-005- Runnable command reference:
docs/specs/WS-CONFORMANCE.md
Environment compatibility:
- Some browser/sandbox runtimes expose
WebSocketbut do not reliably return async stream callbacks to the agent. - If WS streaming is unavailable or unreadable, fall back to polling with equivalent
GETendpoints: WS /agent/ws/read_dms->GET /agent/read_dmsWS /agent/ws/nostr/latest_kind1->GET /agent/nostr/latest_kind1WS /agent/ws/nostr/discovery/latest_kind1->GET /agent/nostr/discovery/latest_kind1WS /agent/ws/nostr/my_latest_kind1->GET /agent/nostr/my_latest_kind1WS /agent/ws/nostr/following/latest_kind1->GET /agent/nostr/following/latest_kind1- Keep the same filters (
nip05,limit,relays) and poll interval policy when usingGET.
Market Order Discovery (Dedicated Path)
Use dedicated endpoint:
GET /agent/market/orders?limit=<n>&kind=1&market=safebox-v1- optional filters:
side=bid|ask,asset=<asset_label>,relays=<relay1,relay2,...>
Behavior:
- Queries followed npubs only.
- Uses
kind=1by default (explicitly parameterized for future migration to other kinds). - Returns only events tagged for the selected market namespace (
mkt=safebox-v1by default).
Follow / Unfollow Management
Safebox core supports following and unfollowing by identifier via:
Acorn.follow(identifier, relay_hint=None, relays=None)Acorn.unfollow(identifier, relays=None)
Accepted identifiers:
- NIP-05 (
name@domain) npub1...- 64-char pubhex
Suggested workflow:
- Follow identity (core or API route, if exposed).
- Query
GET /agent/nostr/following/latest_kind1to verify feed changes. - Unfollow identity when needed and re-check feed.
Kind-0 Profile Lookup by Identifier
Use agent endpoint:
GET /agent/nostr/kind0?identifier=<value>- optional:
&relays=<relay1,relay2,...>
Accepted identifier inputs:
- NIP-05 (
name@domain) npub1...- 64-char pubhex
Returns latest kind-0 event data with parsed JSON profile content:
profile_event.idprofile_event.pubkeyprofile_event.created_atprofile_event.content(object)
Use this when an agent needs authoritative profile metadata before social actions (for example pre-zap context, identity checks, or local profile caching).
Social Identity Preflight (Before DM Flows)
Before running POST /agent/secure_dm or expecting stable sender resolution in clients:
- Ensure kind-0 is fully populated for the sending wallet via
POST /agent/publish_kind0:
namedisplay_name(recommended)about(recommended)picture(recommended)nip05(required for verified identity)lud16(required for zappable identity)
- Identity consistency rule:
lud16SHOULD matchnip05for Safebox-managed identities (same handle/address).- Example:
nip05=lumen@safebox.devandlud16=lumen@safebox.dev.
- Verify profile visibility with
GET /agent/nostr/kind0?identifier=<nip05_or_npub>before DM-heavy workflows.
Operational note:
- Incomplete kind-0 metadata can cause degraded or missing sender identity rendering in some clients and can destabilize DM-adjacent social workflows.
Read Private Messages (NIP-17 Gift Wrap Transport)
Use agent endpoint:
GET /agent/read_dms?limit=<n>&kind=1059- optional relay override:
&relays=<relay1,relay2,...>
Behavior:
- reads incoming gift-wrapped messages using existing wallet record retrieval
- defaults to kind
1059(private DM transport) - returns newest-first messages with bounded
limit
DM streaming variant:
WS /agent/ws/read_dms?limit=<n>&kind=1059&poll_seconds=<n>&access_key=<key>- same
kind/relayssemantics asGET /agent/read_dms - emits
connected,messages, andheartbeatframes
4) Pay Invoice
- Call
POST /agent/pay_invoicewith BOLT11 invoice. - Check
status == OK. - Use returned
balanceas post-payment state; optionally verify withGET /agent/balance.
5) Issue Ecash
- Call
POST /agent/issue_ecashwith sat amount. - Capture returned
ecash_token. - Treat token as bearer value until redeemed.
6) Pay Lightning Address
- Call
POST /agent/pay_lightning_addresswith:
lightning_address(for examplealice@example.com)- either
amount_sats(integer sats) ORamount+currency(floating-point amount in selected currency) - optional
comment,tendered_amount,tendered_currency
- Server performs LNURL resolution and payment using wallet core logic.
- Verify
status == OKand reviewfees_paid. - Use returned
balanceas post-payment state; optionally confirm withGET /agent/balance.
Why prefer this over manual LNURL flow:
- avoids client-side LNURL fetch/parse bugs
- avoids millisat conversion errors
- gives consistent behavior across LN-address providers
- centralizes error handling for unresolved/invalid addresses
7) Accept Ecash
- Call
POST /agent/accept_ecashwithecash_token. - Verify success and
accepted_amount. - Confirm final wallet state via
GET /agent/balance.
8) Zap Event/Profile
- Call
POST /agent/zapwith:
eventorevent_id(one required):note1...,npub1..., NIP-05 (name@domain), or 64-char hex event id- either
amount_satsORamount+currency - optional
comment
- Endpoint resolves target/profile metadata and creates zap request + invoice flow server-side.
- Verify
status == OK. - Confirm post-zap state with returned
balanceand optionallyGET /agent/tx_history.
Notes:
- Use
GET /agent/supported_currenciesbefore fiat-denominated zap requests. - If zap metadata/profile lookup fails, endpoint returns
400withZap failed: ....
Zap by recent-event workflow:
- Fetch recent events:
GET /agent/nostr/latest_kind1?nip05=trbouma@safebox.dev&limit=5
- Pick
events[i].idfrom response. - Zap selected event id:
POST /agent/zapwith{"event_id":"<hex_event_id>","amount_sats":21,"comment":"nice post"}
9) Publish Kind-0 Metadata (NIP-01)
- Call
POST /agent/publish_kind0with any subset of:
name,about,picture- optional:
display_name,nip05,banner,website,lud16 - optional:
extra_fields(object),relays(array)
- Server publishes a kind-0 event and persists the updated profile snapshot in wallet records.
- Confirm returned
event_idand profile fields.
Identity-separation warning:
- Treat each Safebox as a separate social identity surface.
- Do not copy the agent's own stable identity metadata into Safebox profiles if anonymity is desired.
- An agent may operate many Safeboxes with distinct kind-0 identities that should not be trivially correlated back to the controlling agent.
10) Publish Kind-1 Text Note (NIP-01)
- Call
POST /agent/publish_kind1with:
content(required)- optional
relaysarray override
- Server signs and publishes a kind-1 event on configured relays.
- Confirm returned
event_id.
10a) Create Market Order (Bid/Ask, Kind-1)
- Call
POST /agent/market/orderwith:
side:buy/sell(also acceptsbid/ask)asset: market asset label/idmarket: market namespace (mkttag value), defaultsafebox-v1price_sats: integer sats- optional:
quantity,order_id,content,flow,relays
- Server publishes a structured market intent as a kind-1 event.
- Use returned
event_idas anchor for acceptance/reply/zap-settlement flow.
Mentions in posts:
- Preferred format:
nostr:npub1...(NIP-27 URI form). - Fallback format (client-dependent):
@npub1.... - Recommendation: when onboarding a new client/app combination, publish a one-time compatibility post containing both formats and verify rendering on target clients (for example Amethyst/Primal) before standardizing.
Mention helper endpoints:
POST /agent/nostr/format_mention- input:
identifier+ optionalstyle(nostr_uri,at_npub,both) - output: normalized mention string and resolved npub/pubkey
POST /agent/nostr/compose_mentions- input:
base_text,identifiers[], optionalstyle - output: mention-ready post content for direct use with
POST /agent/publish_kind1
11) Send Secure DM (NIP-44 Gift Wrap)
- Call
POST /agent/secure_dmwith:
recipient(required): NIP-05 (name@domain),npub1..., or 64-char pubhexmessage(required): plaintext message to encrypt and send- optional
relaysarray override (defaults to serverPUBLIC_RELAYS)
- Server resolves recipient key, encrypts with wallet
secure_dm, and publishes gift-wrapped DM events. - Confirm
status == OKand inspect returned relay list.
Example:
{"recipient": "alice@example.com","message": "Hello from Safebox agent","relays": ["wss://relay.damus.io", "wss://relay.primal.net"]}
12) Publish Reaction (NIP-25 Kind 7)
- Call
POST /agent/reactwith:
event_id(required): target event id (hex or note id)- optional
content(default❤️) - optional target context:
reacted_pubkey,reacted_kind,relay_hint,a_tag - optional
extra_tagsandrelays
- Server signs and publishes kind-7 reaction tags (
e,p,kwhen available). - Confirm returned
event_idandtags.
Example:
{"event_id": "<hex_event_id>","content": "❤️"}
13) Publish Reply (Kind 1)
- Call
POST /agent/replywith:
event_id(required): target event id (hex or note id)content(required): reply text- optional target context:
target_pubkey,target_kind,relay_hint - optional
extra_tagsandrelays
- Server signs and publishes a kind-1 reply with
e/p/kreply tags. - Confirm returned
event_idandtags.
14) Recipient-First Offer Request (Agent Shows QR)
Use this flow when a human Safebox user will send a grant to the agent wallet by scanning a QR shown by the agent.
- Call
POST /agent/offers/receive/createwith:
- optional
ttl_secondsandcompact_qr(defaulttrue) - optional
include_ascii_qr=trueto receive a terminal-renderable text QR in the same response - optional
grant_kindandgrant_namemetadata (not required for handshake)
- Display
qr_text(orqr_image_url) to the human sender.
- For terminal-native agents, print
ascii_qrwhen requested.
- Sender scans QR from Safebox offer UI.
- Sender transmits grant through existing offer flow.
- Agent waits for grant with:
GET /agent/offers/receive/{intent_id}/wait?timeout_seconds=<n>&poll_seconds=<n>- or websocket stream
WS /agent/ws/offers/receive/{intent_id}?timeout_seconds=<n>&poll_seconds=<n>
Expected response includes:
intent.intent_id,intent.expires_atrecipient.recipient_nauthqr_payload,qr_text,qr_image_url- optional
ascii_qr(present only wheninclude_ascii_qr=true)
Field usage:
- Agent management fields:
status,intent, andrecipient. - Human scan fields:
qr_text(rawrecipient_nauth) orqr_image_url. - Terminal render field:
ascii_qr(human-agent boundary over terminal sessions). - Structured optional context:
qr_payload(for debugging/advanced clients).
Protocol note:
- Recipient-side nauth uses
scope=offer_request. - Scanner routing is expected to detect
offer_requestand redirect into records offer flow instead of generic accept flow.
Flow model note:
- Human-to-human and human-to-agent use the same relay/auth protocol primitives (
21061auth,21062transmittal). - Human-to-agent is intent-driven; handshake completion and record ingest are separate stages.
- Do not treat handshake success as completion until ingest/persist succeeds.
Copy/Paste Quickstart For OpenClaw
BASE_URL="https://skills.example.com"API_KEY="your-wallet-access-key"curl -sS -X POST \-H "X-Access-Key: ${API_KEY}" \-H "Content-Type: application/json" \-d '{"ttl_seconds": 120,"compact_qr": true,"include_ascii_qr": true}' \"${BASE_URL}/agent/offers/receive/create"
Use returned ascii_qr for terminal display and qr_text (raw recipient_nauth) as canonical QR payload content.
Then wait for delivery using the returned intent.intent_id:
INTENT_ID="<intent_id_from_create_response>"curl -sS \-H "X-Access-Key: ${API_KEY}" \"${BASE_URL}/agent/offers/receive/${INTENT_ID}/wait?timeout_seconds=120&poll_seconds=2"
Wait endpoint behavior:
- Returns
status=OKwithgrantwhen a new grant is received during the window. - Returns
status=TIMEOUTwhen no grant arrives before timeout/expiry. - Uses optional
relaysquery override for record lookup.
WebSocket wait behavior:
WS /agent/ws/offers/receive/{intent_id}emits:type=connectedwhen stream is activetype=heartbeatwhile waitingtype=receivedwithgrantwhen delivery is detectedtype=timeoutif no grant arrives before timeout/expiry- Auth the same as other agent websockets:
- preferred:
X-Access-Keyheader - fallback:
?access_key=<key>query parameter - HTTP fallback remains:
GET /agent/offers/receive/{intent_id}/wait
Terminal success criteria (recipient-first):
- Handshake completes for the active intent.
- At least one new transmittal record is observed for the matched presenter/context.
- Record decrypt +
put_recordpersist succeeds. - Wait endpoint/stream reaches terminal receive state (
status=OKwithgrantortype=received).
Troubleshooting guidance:
- If you see handshake logs but no terminal receive state, treat as incomplete ingest.
- If the receiver loop repeats with seen IDs only, regenerate a fresh intent/QR and retry to avoid stale replay windows.
- Timeout without
grantmeans the sender path did not produce a matching transmittal for this intent window. - Validate completion using API state (
wait/WS terminal payload), not sender UI success alone.
Compact behavior:
compact_qr=true(default):qr_textstays rawrecipient_nauth; structured metadata is available inqr_payload.compact_qr=false: QR includes explicit auth/transmittal relay metadata and KEM public metadata.- Backward compatibility:
compactis accepted as an alias for older clients. include_ascii_qr=true: embed text QR directly in create response (preferred for terminal agents).
Fallback text-QR helper:
POST /agent/terminal/ascii_qrwith body:qr_text(required)invert(optional, defaulttrue)- Use when a flow returns only
qr_textand terminal rendering is needed after the fact.
15) Sender-Side Offer Dispatch Lifecycle
Use this flow when the agent is the sender and needs explicit dispatch states.
- Create offer:
POST /agent/offers/createwithgrant_kind,grant_name
- Wait for recipient auth:
GET /agent/offers/{offer_id}/status?wait_seconds=30
- If needed, capture recipient nauth manually:
POST /agent/offers/{offer_id}/capture
- Send grant:
POST /agent/offers/{offer_id}/send
- Check dispatch result:
GET /agent/offers/{offer_id}/delivery
Status semantics:
offer_status:WAITING_RECIPIENT,RECIPIENT_READY,SENDING,SENT,FAILEDdelivery_status:PENDING,DISPATCHED,FAILED
Note:
delivery_status=DISPATCHEDmeans sender-side dispatch completed.- It does not prove recipient-side application-level receipt acknowledgment.
Sender Flow Quick Test (Copy/Paste)
BASE_URL="https://skills.example.com"API_KEY="your-wallet-access-key"GRANT_KIND=34104GRANT_NAME="Passport"RECIPIENT_NAUTH="nauth1..." # optional if using manual capture
- Create offer:
OFFER_ID=$(curl -sS -X POST \-H "X-Access-Key: ${API_KEY}" \-H "Content-Type: application/json" \-d "{\"grant_kind\": ${GRANT_KIND},\"grant_name\": \"${GRANT_NAME}\",\"compact\": true}" \"${BASE_URL}/agent/offers/create" | jq -r '.offer.offer_id')echo "OFFER_ID=${OFFER_ID}"
- Check recipient readiness (or wait):
curl -sS \-H "X-Access-Key: ${API_KEY}" \"${BASE_URL}/agent/offers/${OFFER_ID}/status?wait_seconds=30"
- Optional manual recipient capture:
curl -sS -X POST \-H "X-Access-Key: ${API_KEY}" \-H "Content-Type: application/json" \-d "{\"recipient_nauth\":\"${RECIPIENT_NAUTH}\"}" \"${BASE_URL}/agent/offers/${OFFER_ID}/capture"
- Send offer/grant:
curl -sS -X POST \-H "X-Access-Key: ${API_KEY}" \-H "Content-Type: application/json" \-d '{}' \"${BASE_URL}/agent/offers/${OFFER_ID}/send"
- Confirm dispatch lifecycle:
curl -sS \-H "X-Access-Key: ${API_KEY}" \"${BASE_URL}/agent/offers/${OFFER_ID}/delivery?wait_seconds=10"
Expected terminal state:
offer_statusshould beSENT(orFAILED)delivery_statusshould beDISPATCHED(orFAILED)
Error Handling
400: invalid payload or business-rule failure (insufficient funds, malformed token, invoice failure)401: missing/invalidX-Access-Key403: invalid invite code (onboarding)409: onboarding registration conflict; retry safely500: transient server/wallet load error; retry with backoff
Retry guidance:
- Use bounded exponential backoff for
500and network failures. - Do not blindly retry non-idempotent payment operations without reconciliation checks.
- For invoice receive flows, prefer
GET /agent/invoice_status/{quote}before concluding failure. - For recipient-first offer requests, regenerate a fresh QR if
expires_athas passed.
Payment Reliability and Resilience (Required)
All payment-capable flows in this skill MUST follow:
docs/specs/PAYMENT-ERROR-HANDLING-AND-RESILIENCE.md
Operational rules:
- Treat payment lifecycle as staged:
ACCEPTED -> PROCESSING -> SETTLED -> NOTIFIED- failures MUST end in
FAILEDorUNCERTAIN(not false success)
- Before destructive proof mutation workflows, run:
GET /agent/proof_safety_audit- optional deeper check:
GET /agent/proof_safety_audit?check_relay=true
- On
audit.safe_to_swap=false, stop and reconcile first. Do not continue swap/consolidate-like actions. - Do not report payment success unless settlement is confirmed.
- For uncertain post-debit delivery outcomes (for example ecash delivery failure), classify as
UNCERTAINand preserve recovery artifacts for reconciliation. - Keep retries bounded and explicit; do not blindly retry non-idempotent monetary operations.
- Use structured logs for payment status transitions and error class; never log secrets/token material.
Guardrails
- Never log
access_key,nsec,seed_phrase,ecash_token, or full invoices in plaintext logs. - Always use HTTPS in production.
- Store recovery materials separately from operational API credentials.
- Prefer explicit status checks over optimistic assumptions.
References
docs/specs/AGENT-API.mddocs/specs/AGENT-FLOWS.mddocs/specs/AGENT-OFFER-RECIPIENT-FIRST-FLOW.mddocs/specs/PAYMENT-ERROR-HANDLING-AND-RESILIENCE.mdapp/routers/agent.pysafebox/cli_agent.pysafebox/cli_acorn.py