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_handle assignment,
name, display_name, nip05, and lud16 setup.
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_code for onboarding
access_key for authenticated wallet actions
invoice string for pay flow
lightning_address and amount_sats for direct LN-address pay flow
event and amount_sats (or amount + currency) for zap flow
amount (sat integer) for create/issue flows
ecash_token for 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=1059 is 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
agent for anything that maps to documented /agent/* endpoints.
- Use
acorn for local core tasks not exposed through /agent/*.
Non-interference rule:
- Do not modify
safebox/cli_acorn.py or safebox/acorn.py when extending agent CLI flows.
Canonical Endpoints
POST /agent/onboard
GET /agent/info
GET /agent/whoami
GET /agent/balance
GET /agent/tx_history
GET /agent/proof_safety_audit
GET /agent/supported_currencies
POST /agent/set_custom_handle
GET /agent/read_dms
WS /agent/ws/read_dms
WS /agent/ws/offers/receive/{intent_id}
GET /agent/nostr/latest_kind1
GET /agent/nostr/discovery/latest_kind1
GET /agent/nostr/my_latest_kind1
GET /agent/nostr/zap_receipts
GET /agent/nostr/replies
GET /agent/nostr/kind0
GET /agent/nostr/followers
GET /agent/nostr/following/latest_kind1
WS /agent/ws/nostr/latest_kind1
WS /agent/ws/nostr/discovery/latest_kind1
WS /agent/ws/nostr/my_latest_kind1
WS /agent/ws/nostr/following/latest_kind1
GET /agent/market/orders
POST /agent/nostr/format_mention
POST /agent/nostr/compose_mentions
POST /agent/create_invoice
GET /agent/invoice_status/{quote}
POST /agent/pay_invoice
POST /agent/pay_lightning_address
POST /agent/zap
POST /agent/publish_kind0
POST /agent/publish_kind1
POST /agent/delete_request
POST /agent/market/order
POST /agent/market/ms02/generate_entitlement
POST /agent/market/ms02/encrypt_entitlement_nip44
POST /agent/market/ms02/decrypt_entitlement_nip44
POST /agent/market/ms02/validate_buyer_delivery
POST /agent/market/ms02/generate_wrapper
POST /agent/market/ms02/derive_wrapper_commitment
POST /agent/market/ms02/construct_ask
POST /agent/market/ms02/publish_ask
POST /agent/market/ms02/parse_ask_event
GET /agent/market/ms02/asks
GET /agent/market/ms02/settlement_receipts
GET /agent/market/ms02/clear_order
POST /agent/market/ms02/deliver_wrapper_secret
POST /agent/market/secret_hash/derive
POST /agent/market/secret_hash/verify
POST /agent/secure_dm
POST /agent/react
POST /agent/reply
POST /agent/follow
POST /agent/unfollow
POST /agent/issue_ecash
POST /agent/accept_ecash
POST /agent/terminal/ascii_qr
POST /agent/offers/receive/create
GET /agent/offers/receive/{intent_id}/wait
POST /agent/offers/create
GET /agent/offers/{offer_id}/status
POST /agent/offers/{offer_id}/capture
POST /agent/offers/{offer_id}/send
GET /agent/offers/{offer_id}/delivery
Execution Recipes
1) Onboard Wallet
- Call
POST /agent/onboard with invite_code.
- Persist returned (REQUIRED before any further action):
wallet.access_key
wallet.nsec
wallet.seed_phrase
wallet.emergency_code
- Treat response as sensitive secret material.
- Immediately verify persistence:
- confirm
wallet.access_key is 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_ONBOARDING and retry/save before any production use.
- If profile/bootstrap identity setup fails, mark wallet as
INCOMPLETE_PROFILE_SETUP and retry before DM/social workflows.
Expected response includes:
wallet.handle, wallet.npub, wallet.home_relay
session.access_token (optional for external systems; agent calls should still use X-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_kind0 with at least:
name
picture (avatar/logo URL)
nip05
lud16
- Recommended default avatar source: RoboHash URL for deterministic agent identity images.
- Example:
https://robohash.org/<stable-agent-seed>?set=set4
- Identity rule:
nip05 MUST equal lud16 for Safebox-managed agent identities.
- Example:
lumen@safebox.dev for 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_handle
name
display_name
lud16
- MS-02 seller/buyer preparation
2) Read Wallet State
- Call
GET /agent/info with X-Access-Key.
- Includes
lightning_address derived from request host.
- Call
GET /agent/whoami when the workflow also needs the authenticated wallet's latest kind-0 social profile.
- Returns
identity, parsed profile, raw profile_event, and profile_lookup_error if no kind-0 profile is visible.
- Call
GET /agent/balance for lightweight polling or confirmation.
- Call
GET /agent/tx_history for recent transaction audit context.
2a) Set Wallet Custom Handle
- Call
POST /agent/set_custom_handle with:
custom_handle (required): desired local-part for wallet lightning address.
- Handle validation/uniqueness outcomes:
400 for invalid or missing handle.
409 when the handle is already taken.
- Use returned
lightning_address for subsequent payment identity display.
3) Create Invoice (Receive Payment)
- Call
POST /agent/create_invoice with sat amount and optional comment.
- Return invoice immediately to payer.
- Use returned
quote and status_path to monitor settlement:
- poll
GET /agent/invoice_status/{quote}
- terminal state is
quote_status: PAID
- Optionally confirm final wallet state with
GET /agent/balance or GET /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_ref
wrapper_commitment
- Call
POST /agent/market/ms02/construct_ask with:
- required:
wrapper_ref, price_sats, expiry, wrapper_commitment
- optional:
wrapper_scheme (defaults to nostr_keypair_v1)
- optional:
fulfillment_mode (defaults to provider_resolved_v1)
- required for decryptable delivery:
sealed_delivery_alg, encrypted_entitlement
- optional:
content_format (yaml default, plain supported)
- Use returned:
content (human-readable ask preview)
tags, order_details_jcs, and ask_id (authoritative machine data)
- Publish the constructed ask with
POST /agent/market/ms02/publish_ask using the returned content and exact tags.
Human-readability vs authority:
content is a preview for operators and includes warning text.
- Long identifiers are shortened in YAML display fields (
*_display).
- Machines MUST parse/verify from
tags and order_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
kind when 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, or EXPIRED
- 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
tags and order_details_jcs as authoritative
- agents MUST NOT treat the human-readable
content preview 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_code
entitlement_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_entitlement for buyer_decryptable_v1
- encrypt the provider-native entitlement material to the wrapper public key
Inputs:
wrapper_ref
entitlement_code
entitlement_secret
Returns:
sealed_delivery_alg=nip44_v2
encrypted_entitlement
plaintext_payload_jcs
Usage rule:
- use the returned
sealed_delivery_alg and encrypted_entitlement in POST /agent/market/ms02/construct_ask when fulfillment_mode=buyer_decryptable_v1
3d) MS-02 Buyer Decrypt + Validation
Agent API helper endpoints:
POST /agent/market/ms02/decrypt_entitlement_nip44
POST /agent/market/ms02/validate_buyer_delivery
Purpose:
- let the buyer recover
entitlement_code and entitlement_secret from encrypted_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 ask event
- optional
sender_pubkey when decrypting without ask lookup
- returns:
decrypted_entitlement
plaintext_payload_jcs
- resolved
wrapper_ref
validate_buyer_delivery:
- input:
wrapper_secret_nsec
ask_event_id or full ask event
- verifies:
- derived
wrapper_ref matches the ask
- recomputed
wrapper_commitment matches the ask
- decrypted entitlement contains:
entitlement_code
entitlement_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_v1
wrapper_ref
wrapper_secret_nsec
wrapper_commitment_hint
Usage note:
wrapper_secret_nsec is sensitive and should not be logged
wrapper_commitment_hint is 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_commitment from:
- wrapper secret
entitlement_code
entitlement_secret
Returns:
wrapper_ref
wrapper_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
nsec string
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/derive
POST /agent/market/secret_hash/verify
Purpose:
- derive a canonical market
secret_hash from 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_hash as 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
SAT if 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
nip05 target MUST already be present in the wallet kind-3 follow list.
- If not followed, endpoint returns
403.
- Read returned
events[] and choose the target event_id (or event_id_hex / id).
- Pass that value as
event_id (or event) in POST /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_request with at least one reference:
event_ids (list of note1... or 64-char hex ids), and/or
a_tags (list of NIP-01 coordinates: <kind>:<pubkey>:<d-identifier>).
- Optionally include:
kinds (list of integers; recommended when known)
reason (free text, becomes kind-5 content)
relays override
- 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_id for 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
9735 receipts filtered by #e=<event_id>.
- For each receipt, inspect:
zapper_pubkey / zapper_npub (derived from zap request description.pubkey, fallback P tag)
zapper_identity_source to confirm identity provenance
lnurl_provider_pubkey / lnurl_provider_npub are receipt signer identities, not zapper identities
zap_request_raw (original embedded kind-9734 JSON string)
zap_request (parsed embedded kind-9734 object)
zap_amount_msat and invoice_amount_msat
amount_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_mention on zapper_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
1 events filtered by #e=<event_id>.
- Use returned fields:
replies[].event_id
replies[].pubkey
replies[].content
replies[].reply_to_event_ids
replies[].is_direct_reply
- Use
is_direct_reply=true when 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_id plus optional:
content (+, -, emoji, or :shortcode:)
reacted_pubkey
reacted_kind
relay_hint
a_tag (for addressable target coordinates)
extra_tags
Behavior:
- Publishes a kind
7 event with target e/p tags.
- If multiple
e/p tags are present, target e/p are placed last for NIP-25 compatibility guidance.
B) React to external content (kind 17)
- Omit
event_id.
- Provide:
external_tags including at least one k tag and one i tag
- optional
content, extra_tags
Example external tags:
["k","web"]
["i","https://example.com"]
Behavior:
- Publishes a kind
17 event 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_id for 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> (default 100, max 500)
strict=true|false (default true)
relays=<relay1,relay2,...>
- Response fields include:
target_identifier
target_pubkey
count
followers[] with follower_pubkey, follower_npub, event_id, created_at, relay_hint
Strict mode behavior:
strict=true verifies each candidate follower against their latest known kind-3 contacts event to reduce stale false positives.
strict=false is 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_kind1 only streams for identifiers in the wallet kind-3 follow list.
ws/nostr/discovery/latest_kind1 is unrestricted by follow-list and intended for open lookup.
- For broad discovery, use
ws/nostr/following/latest_kind1.
Auth notes:
- Preferred: send
X-Access-Key header during websocket handshake.
- Fallback: pass
access_key query parameter if client cannot set websocket headers.
Stream payload types:
type=connected: connection accepted and stream initialized
type=events: changed event set detected, includes full events[]
type=heartbeat: no change detected at this poll interval
Post-deploy validation:
- Run WebSocket smoke checks in
docs/specs/AGENT-API.md:
WS-CONN-001
WS-DATA-002
WS-HEARTBEAT-003
WS-POLICY-004
WS-FALLBACK-005
- Runnable command reference:
docs/specs/WS-CONFORMANCE.md
Environment compatibility:
- Some browser/sandbox runtimes expose
WebSocket but do not reliably return async stream callbacks to the agent.
- If WS streaming is unavailable or unreadable, fall back to polling with equivalent
GET endpoints:
WS /agent/ws/read_dms -> GET /agent/read_dms
WS /agent/ws/nostr/latest_kind1 -> GET /agent/nostr/latest_kind1
WS /agent/ws/nostr/discovery/latest_kind1 -> GET /agent/nostr/discovery/latest_kind1
WS /agent/ws/nostr/my_latest_kind1 -> GET /agent/nostr/my_latest_kind1
WS /agent/ws/nostr/following/latest_kind1 -> GET /agent/nostr/following/latest_kind1
- Keep the same filters (
nip05, limit, relays) and poll interval policy when using GET.
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=1 by default (explicitly parameterized for future migration to other kinds).
- Returns only events tagged for the selected market namespace (
mkt=safebox-v1 by 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_kind1 to 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.id
profile_event.pubkey
profile_event.created_at
profile_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:
name
display_name (recommended)
about (recommended)
picture (recommended)
nip05 (required for verified identity)
lud16 (required for zappable identity)
- Identity consistency rule:
lud16 SHOULD match nip05 for Safebox-managed identities (same handle/address).
- Example:
nip05=lumen@safebox.dev and lud16=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/relays semantics as GET /agent/read_dms
- emits
connected, messages, and heartbeat frames
4) Pay Invoice
- Call
POST /agent/pay_invoice with BOLT11 invoice.
- Check
status == OK.
- Use returned
balance as post-payment state; optionally verify with GET /agent/balance.
5) Issue Ecash
- Call
POST /agent/issue_ecash with sat amount.
- Capture returned
ecash_token.
- Treat token as bearer value until redeemed.
6) Pay Lightning Address
- Call
POST /agent/pay_lightning_address with:
lightning_address (for example alice@example.com)
- either
amount_sats (integer sats) OR amount + 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 == OK and review fees_paid.
- Use returned
balance as post-payment state; optionally confirm with GET /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_ecash with ecash_token.
- Verify success and
accepted_amount.
- Confirm final wallet state via
GET /agent/balance.
8) Zap Event/Profile
- Call
POST /agent/zap with:
event or event_id (one required): note1..., npub1..., NIP-05 (name@domain), or 64-char hex event id
- either
amount_sats OR amount + 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
balance and optionally GET /agent/tx_history.
Notes:
- Use
GET /agent/supported_currencies before fiat-denominated zap requests.
- If zap metadata/profile lookup fails, endpoint returns
400 with Zap failed: ....
Zap by recent-event workflow:
- Fetch recent events:
GET /agent/nostr/latest_kind1?nip05=trbouma@safebox.dev&limit=5
- Pick
events[i].id from response.
- Zap selected event id:
POST /agent/zap with {"event_id":"<hex_event_id>","amount_sats":21,"comment":"nice post"}
9) Publish Kind-0 Metadata (NIP-01)
- Call
POST /agent/publish_kind0 with 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_id and 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_kind1 with:
content (required)
- optional
relays array 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/order with:
side: buy/sell (also accepts bid/ask)
asset: market asset label/id
market: market namespace (mkt tag value), default safebox-v1
price_sats: integer sats
- optional:
quantity, order_id, content, flow, relays
- Server publishes a structured market intent as a kind-1 event.
- Use returned
event_id as 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 + optional style (nostr_uri, at_npub, both)
- output: normalized mention string and resolved npub/pubkey
POST /agent/nostr/compose_mentions
- input:
base_text, identifiers[], optional style
- 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_dm with:
recipient (required): NIP-05 (name@domain), npub1..., or 64-char pubhex
message (required): plaintext message to encrypt and send
- optional
relays array override (defaults to server PUBLIC_RELAYS)
- Server resolves recipient key, encrypts with wallet
secure_dm, and publishes gift-wrapped DM events.
- Confirm
status == OK and 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/react with:
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_tags and relays
- Server signs and publishes kind-7 reaction tags (
e, p, k when available).
- Confirm returned
event_id and tags.
Example:
{
"event_id": "<hex_event_id>",
"content": "❤️"
}
13) Publish Reply (Kind 1)
- Call
POST /agent/reply with:
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_tags and relays
- Server signs and publishes a kind-1 reply with
e/p/k reply tags.
- Confirm returned
event_id and tags.
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/create with:
- optional
ttl_seconds and compact_qr (default true)
- optional
include_ascii_qr=true to receive a terminal-renderable text QR in the same response
- optional
grant_kind and grant_name metadata (not required for handshake)
- Display
qr_text (or qr_image_url) to the human sender.
- For terminal-native agents, print
ascii_qr when 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_at
recipient.recipient_nauth
qr_payload, qr_text, qr_image_url
- optional
ascii_qr (present only when include_ascii_qr=true)
Field usage:
- Agent management fields:
status, intent, and recipient.
- Human scan fields:
qr_text (raw recipient_nauth) or qr_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_request and 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 (
21061 auth, 21062 transmittal).
- 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=OK with grant when a new grant is received during the window.
- Returns
status=TIMEOUT when no grant arrives before timeout/expiry.
- Uses optional
relays query override for record lookup.
WebSocket wait behavior:
WS /agent/ws/offers/receive/{intent_id} emits:
type=connected when stream is active
type=heartbeat while waiting
type=received with grant when delivery is detected
type=timeout if no grant arrives before timeout/expiry
- Auth the same as other agent websockets:
- preferred:
X-Access-Key header
- 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_record persist succeeds.
- Wait endpoint/stream reaches terminal receive state (
status=OK with grant or type=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
grant means 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_text stays raw recipient_nauth; structured metadata is available in qr_payload.
compact_qr=false: QR includes explicit auth/transmittal relay metadata and KEM public metadata.
- Backward compatibility:
compact is 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_qr with body:
qr_text (required)
invert (optional, default true)
- Use when a flow returns only
qr_text and 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/create with grant_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, FAILED
delivery_status: PENDING, DISPATCHED, FAILED
Note:
delivery_status=DISPATCHED means 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=34104
GRANT_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_status should be SENT (or FAILED)
delivery_status should be DISPATCHED (or FAILED)
Error Handling
400: invalid payload or business-rule failure (insufficient funds, malformed token, inv
…(truncated)
1---2name: trbouma-safebox-safebox3description: Agent API Skill4---5# Agent API Skill67## Bootstrap89When starting a new session, fetch these files before executing any workflows:1011| File | Path |12|------|-----|13| This skill | `skills/agent-api/SKILL.md` |14| MS-02 Market Spec | `docs/specs/mkt/MS-02-entitlement-market.md` |15| MS-02 Scenario | `docs/specs/mkt/MS-02-END-TO-END-SCENARIO.md` |16| MS-02 Conformance | `docs/specs/mkt/MS-02-CONFORMANCE.md` |17| MS-02 Operator Guide | `docs/operators/ms02-agent-operator-guide.md` |18| WS Conformance | `docs/specs/WS-CONFORMANCE.md` |1920Resolve paths from the repository root of the current checkout (current branch).21If using HTTP fetches, map these paths onto the same branch/source used to load this skill.22Do not rely on cached or summarized versions.2324Do not load `MS-01` by default. It is deprecated and retained only as a legacy reference.2526For agent-operated MS-02 trading, treat the operator guide as the canonical workflow document for:2728- seller/buyer role setup,29- end-to-end trade execution,30- kind-0 social profile publication,31- `custom_handle` assignment,32- `name`, `display_name`, `nip05`, and `lud16` setup.3334## Purpose3536Use this skill when an autonomous agent needs to operate a Safebox wallet through the header-authenticated Agent API (no browser cookies, no interactive UI).3738Primary outcomes:3940- Onboard a wallet from invite code41- Read wallet info and balance42- Read transaction history43- Create and pay Lightning invoices44- Pay Lightning addresses directly45- Execute zaps to notes/profiles via agent endpoint46- Send secure direct messages to npub/NIP-05 recipients47- Issue and accept Cashu ecash tokens48- Create recipient-first offer QR payloads so humans can send grants by scanning agent QR4950## Inputs5152Required:5354- `base_url` (example: `https://skills.example.com`)5556Conditional:5758- `invite_code` for onboarding59- `access_key` for authenticated wallet actions60- `invoice` string for pay flow61- `lightning_address` and `amount_sats` for direct LN-address pay flow62- `event` and `amount_sats` (or `amount` + `currency`) for zap flow63- `amount` (sat integer) for create/issue flows64- `ecash_token` for accept flow6566## Auth Model6768- Authenticated calls require header: `X-Access-Key: <access_key>`69- Onboarding does not require `X-Access-Key`; it returns new credentials7071## DM Paths (Quick Reference)7273Use these exact paths for private messaging flows:7475- Read DMs: `GET /agent/read_dms?limit=<n>&kind=1059`76- Read replies to event: `GET /agent/nostr/replies?event_id=<event_id>&limit=<n>`77- Send secure DM: `POST /agent/secure_dm`78- Stream DMs (WS): `WS /agent/ws/read_dms?limit=<n>&kind=1059`7980Minimum read example:8182```bash83curl -sS \84 -H "X-Access-Key: ${API_KEY}" \85 "${BASE_URL}/agent/read_dms?limit=20&kind=1059"86```8788Notes:8990- `kind=1059` is the default private DM transport for agent reads.91- If inbox appears empty, retry with explicit relay override:92 `GET /agent/read_dms?limit=20&kind=1059&relays=wss://relay.getsafebox.app,wss://relay.damus.io,wss://relay.primal.net`9394## CLI Surfaces (Use Both)9596This repo now has two CLI entry points. Agents may use either, but should choose based on task:9798- `acorn` / `safebox` (`safebox/cli_acorn.py`):99 - local wallet/core operations100 - direct Acorn behaviors101 - legacy/manual operator workflows102- `agent` (`safebox/cli_agent.py`):103 - header-authenticated `/agent/*` API workflows104 - market endpoints (`/agent/market/order`, `/agent/market/orders`)105 - DM/read_dms/zap receipt automation flows106107Selection rule:108109- Prefer `agent` for anything that maps to documented `/agent/*` endpoints.110- Use `acorn` for local core tasks not exposed through `/agent/*`.111112Non-interference rule:113114- Do not modify `safebox/cli_acorn.py` or `safebox/acorn.py` when extending `agent` CLI flows.115116## Canonical Endpoints117118- `POST /agent/onboard`119- `GET /agent/info`120- `GET /agent/whoami`121- `GET /agent/balance`122- `GET /agent/tx_history`123- `GET /agent/proof_safety_audit`124- `GET /agent/supported_currencies`125- `POST /agent/set_custom_handle`126- `GET /agent/read_dms`127- `WS /agent/ws/read_dms`128- `WS /agent/ws/offers/receive/{intent_id}`129- `GET /agent/nostr/latest_kind1`130- `GET /agent/nostr/discovery/latest_kind1`131- `GET /agent/nostr/my_latest_kind1`132- `GET /agent/nostr/zap_receipts`133- `GET /agent/nostr/replies`134- `GET /agent/nostr/kind0`135- `GET /agent/nostr/followers`136- `GET /agent/nostr/following/latest_kind1`137- `WS /agent/ws/nostr/latest_kind1`138- `WS /agent/ws/nostr/discovery/latest_kind1`139- `WS /agent/ws/nostr/my_latest_kind1`140- `WS /agent/ws/nostr/following/latest_kind1`141- `GET /agent/market/orders`142- `POST /agent/nostr/format_mention`143- `POST /agent/nostr/compose_mentions`144- `POST /agent/create_invoice`145- `GET /agent/invoice_status/{quote}`146- `POST /agent/pay_invoice`147- `POST /agent/pay_lightning_address`148- `POST /agent/zap`149- `POST /agent/publish_kind0`150- `POST /agent/publish_kind1`151- `POST /agent/delete_request`152- `POST /agent/market/order`153- `POST /agent/market/ms02/generate_entitlement`154- `POST /agent/market/ms02/encrypt_entitlement_nip44`155- `POST /agent/market/ms02/decrypt_entitlement_nip44`156- `POST /agent/market/ms02/validate_buyer_delivery`157- `POST /agent/market/ms02/generate_wrapper`158- `POST /agent/market/ms02/derive_wrapper_commitment`159- `POST /agent/market/ms02/construct_ask`160- `POST /agent/market/ms02/publish_ask`161- `POST /agent/market/ms02/parse_ask_event`162- `GET /agent/market/ms02/asks`163- `GET /agent/market/ms02/settlement_receipts`164- `GET /agent/market/ms02/clear_order`165- `POST /agent/market/ms02/deliver_wrapper_secret`166- `POST /agent/market/secret_hash/derive`167- `POST /agent/market/secret_hash/verify`168- `POST /agent/secure_dm`169- `POST /agent/react`170- `POST /agent/reply`171- `POST /agent/follow`172- `POST /agent/unfollow`173- `POST /agent/issue_ecash`174- `POST /agent/accept_ecash`175- `POST /agent/terminal/ascii_qr`176- `POST /agent/offers/receive/create`177- `GET /agent/offers/receive/{intent_id}/wait`178- `POST /agent/offers/create`179- `GET /agent/offers/{offer_id}/status`180- `POST /agent/offers/{offer_id}/capture`181- `POST /agent/offers/{offer_id}/send`182- `GET /agent/offers/{offer_id}/delivery`183184## Execution Recipes185186### 1) Onboard Wallet1871881. Call `POST /agent/onboard` with `invite_code`.1892. Persist returned (REQUIRED before any further action):190 - `wallet.access_key`191 - `wallet.nsec`192 - `wallet.seed_phrase`193 - `wallet.emergency_code`1943. Treat response as sensitive secret material.1954. Immediately verify persistence:196 - confirm `wallet.access_key` is non-empty and saved in the agent’s durable identity store197 - confirm handle + access_key mapping is stored as an atomic pair198 - do not proceed to other API calls until the key is confirmed saved199200Operational guardrail:201202- If onboarding succeeds but key persistence fails, mark wallet as `INCOMPLETE_ONBOARDING` and retry/save before any production use.203- If profile/bootstrap identity setup fails, mark wallet as `INCOMPLETE_PROFILE_SETUP` and retry before DM/social workflows.204205Expected response includes:206207- `wallet.handle`, `wallet.npub`, `wallet.home_relay`208- `session.access_token` (optional for external systems; agent calls should still use `X-Access-Key`)209210### 1a) Immediate Social Identity Setup (Required)211212Right after onboarding, before social actions:2132141. Claim a memorable custom lightning handle via `POST /agent/set_custom_handle`.215 - Choose a human-rememberable local-part (for example `lumen`, `nova`, `pixel`).2162. Publish kind-0 profile via `POST /agent/publish_kind0` with at least:217 - `name`218 - `picture` (avatar/logo URL)219 - `nip05`220 - `lud16`221 - Recommended default avatar source: RoboHash URL for deterministic agent identity images.222 - Example: `https://robohash.org/<stable-agent-seed>?set=set4`2233. Identity rule:224 - `nip05` MUST equal `lud16` for Safebox-managed agent identities.225 - Example: `lumen@safebox.dev` for both fields.2264. Verify profile visibility:227 - `GET /agent/nostr/kind0?identifier=<nip05>`228229Operational guardrail:230231- Do not run DM-first or mention/zap response workflows until kind-0 includes valid `nip05` + `lud16` + `picture`.232233Operator note:234235- For the full identity and operator-facing setup procedure, use:236 - `docs/operators/ms02-agent-operator-guide.md`237- That guide is the canonical reference for:238 - `custom_handle`239 - `name`240 - `display_name`241 - `lud16`242 - MS-02 seller/buyer preparation243244### 2) Read Wallet State2452461. Call `GET /agent/info` with `X-Access-Key`.247 - Includes `lightning_address` derived from request host.2482. Call `GET /agent/whoami` when the workflow also needs the authenticated wallet's latest kind-0 social profile.249 - Returns `identity`, parsed `profile`, raw `profile_event`, and `profile_lookup_error` if no kind-0 profile is visible.2503. Call `GET /agent/balance` for lightweight polling or confirmation.2514. Call `GET /agent/tx_history` for recent transaction audit context.252253### 2a) Set Wallet Custom Handle2542551. Call `POST /agent/set_custom_handle` with:256 - `custom_handle` (required): desired local-part for wallet lightning address.2572. Handle validation/uniqueness outcomes:258 - `400` for invalid or missing handle.259 - `409` when the handle is already taken.2603. Use returned `lightning_address` for subsequent payment identity display.261262### 3) Create Invoice (Receive Payment)2632641. Call `POST /agent/create_invoice` with sat amount and optional comment.2652. Return invoice immediately to payer.2663. Use returned `quote` and `status_path` to monitor settlement:267 - poll `GET /agent/invoice_status/{quote}`268 - terminal state is `quote_status: PAID`2694. Optionally confirm final wallet state with `GET /agent/balance` or `GET /agent/tx_history`.270271### 3a) MS-02 Ask Construction (Generic Entitlement Profile)272273Use the dedicated constructor before publishing an MS-02 ask:2742751. Prepare entitlement profile artifacts:276 - `wrapper_ref`277 - `wrapper_commitment`2782. Call `POST /agent/market/ms02/construct_ask` with:279 - required: `wrapper_ref`, `price_sats`, `expiry`, `wrapper_commitment`280 - optional: `wrapper_scheme` (defaults to `nostr_keypair_v1`)281 - optional: `fulfillment_mode` (defaults to `provider_resolved_v1`)282 - required for decryptable delivery: `sealed_delivery_alg`, `encrypted_entitlement`283 - optional: `content_format` (`yaml` default, `plain` supported)2843. Use returned:285 - `content` (human-readable ask preview)286 - `tags`, `order_details_jcs`, and `ask_id` (authoritative machine data)2874. Publish the constructed ask with `POST /agent/market/ms02/publish_ask` using the returned `content` and exact `tags`.288289Human-readability vs authority:290291- `content` is a preview for operators and includes warning text.292- Long identifiers are shortened in YAML display fields (`*_display`).293- Machines MUST parse/verify from `tags` and `order_details_jcs`, not from preview text.294295Example:296297```bash298curl -sS -X POST \299 -H "X-Access-Key: ${SELLER_KEY}" \300 -H "Content-Type: application/json" \301 -d '{302 "wrapper_ref":"npub1...",303 "price_sats":21,304 "expiry":"2026-03-31T23:59:59Z",305 "wrapper_commitment":"7f3a9c2d41b8d4479c31c6f3a4b7a1e1d0f9d8c7b6a5e4d3c2b1a09182736455"306 }' \307 "${BASE_URL}/agent/market/ms02/construct_ask"308```309310Publish example:311312```bash313curl -sS -X POST \314 -H "X-Access-Key: ${SELLER_KEY}" \315 -H "Content-Type: application/json" \316 -d '{317 "content":"ASK #MS02 ...",318 "tags":[["mkt","MS-02"],["side","ask"]],319 "kind":1320 }' \321 "${BASE_URL}/agent/market/ms02/publish_ask"322```323324Consumer-side helpers:325326- `GET /agent/market/ms02/asks`327 - list published MS-02 asks from relays328 - defaults to `kind=1`329 - accepts an alternate `kind` when needed330- `POST /agent/market/ms02/parse_ask_event`331 - parse and validate a full ask event or fetch by `event_id`332- `GET /agent/market/ms02/settlement_receipts`333 - fetch zap receipts for a published ask event id334- `GET /agent/market/ms02/clear_order`335 - evaluate receipts and determine whether the ask is `OPEN`, `CLEARED`, or `EXPIRED`336 - when cleared, identify the winning buyer deterministically337- `POST /agent/market/ms02/deliver_wrapper_secret`338 - deliver the wrapper secret to the cleared winner via secure DM339340Consumer rule:341342- agents MUST treat `tags` and `order_details_jcs` as authoritative343- agents MUST NOT treat the human-readable `content` preview as the source of truth344345### 3b) MS-02 Entitlement Generator346347Agent API helper endpoint:348349- `POST /agent/market/ms02/generate_entitlement`350351Purpose:352353- generate provider-native entitlement inputs for the MS-02 wrapper flow354- normalize explicitly provided entitlement inputs without changing them355356Behavior:357358- if no values are passed, returns a generated test entitlement359- if one value is missing, generates only the missing value360- if both values are passed, returns the normalized values361362Returns:363364- `entitlement_code`365- `entitlement_secret`366- generation flags indicating whether test/default values were created367368### 3c) MS-02 NIP-44 Entitlement Encryption369370Agent API helper endpoint:371372- `POST /agent/market/ms02/encrypt_entitlement_nip44`373374Purpose:375376- produce `encrypted_entitlement` for `buyer_decryptable_v1`377- encrypt the provider-native entitlement material to the wrapper public key378379Inputs:380381- `wrapper_ref`382- `entitlement_code`383- `entitlement_secret`384385Returns:386387- `sealed_delivery_alg=nip44_v2`388- `encrypted_entitlement`389- `plaintext_payload_jcs`390391Usage rule:392393- use the returned `sealed_delivery_alg` and `encrypted_entitlement` in `POST /agent/market/ms02/construct_ask` when `fulfillment_mode=buyer_decryptable_v1`394395### 3d) MS-02 Buyer Decrypt + Validation396397Agent API helper endpoints:398399- `POST /agent/market/ms02/decrypt_entitlement_nip44`400- `POST /agent/market/ms02/validate_buyer_delivery`401402Purpose:403404- let the buyer recover `entitlement_code` and `entitlement_secret` from `encrypted_entitlement`405- let the buyer verify that the delivered wrapper secret actually matches the published ask406407`decrypt_entitlement_nip44`:408409- input:410 - `wrapper_secret_nsec`411 - either `encrypted_entitlement`, `ask_event_id`, or a full ask `event`412 - optional `sender_pubkey` when decrypting without ask lookup413- returns:414 - `decrypted_entitlement`415 - `plaintext_payload_jcs`416 - resolved `wrapper_ref`417418`validate_buyer_delivery`:419420- input:421 - `wrapper_secret_nsec`422 - `ask_event_id` or full ask `event`423- verifies:424 - derived `wrapper_ref` matches the ask425 - recomputed `wrapper_commitment` matches the ask426 - decrypted entitlement contains:427 - `entitlement_code`428 - `entitlement_secret`429430### 3e) MS-02 Wrapper Generator431432Agent API helper endpoint:433434- `POST /agent/market/ms02/generate_wrapper`435436Purpose:437438- generate a fresh Nostr-native trading wrapper for MS-02439- or normalize an explicitly supplied `nsec`440441Returns:442443- `wrapper_scheme=nostr_keypair_v1`444- `wrapper_ref`445- `wrapper_secret_nsec`446- `wrapper_commitment_hint`447448Usage note:449450- `wrapper_secret_nsec` is sensitive and should not be logged451- `wrapper_commitment_hint` is not the full MS-02 wrapper commitment over entitlement data; it is only a wrapper-key-derived hint452453### 3e) MS-02 Wrapper Commitment Derivation454455Agent API helper endpoint:456457- `POST /agent/market/ms02/derive_wrapper_commitment`458459Purpose:460461- derive the full MS-02 `wrapper_commitment` from:462 - wrapper secret463 - `entitlement_code`464 - `entitlement_secret`465466Returns:467468- `wrapper_ref`469- `wrapper_commitment`470- canonical commitment payload JSON (`commitment_payload_jcs`)471472Implementation rule:473474- this is the authoritative helper for the current MS-02 wrapper-commitment convention475- it binds raw wrapper secret material, not just the delivered `nsec` string476477### 3f) Market Secret Hash Helpers478479Use these helpers when an agent needs deterministic secret-hash derivation or verification without reimplementing the market hashing convention incorrectly.480481Endpoints:482483- `POST /agent/market/secret_hash/derive`484- `POST /agent/market/secret_hash/verify`485486Purpose:487488- derive a canonical market `secret_hash` from stable inputs489- verify that a provided preimage/input set matches a published `secret_hash`490491Usage rule:492493- prefer these helpers when constructing or validating market commitments that must match Safebox market conventions exactly494- treat the full returned `secret_hash` as authoritative495- never use shortened display hashes for matching, clearing, or redemption decisions496497### Currency Preflight (Before Address Payments)4984991. Call `GET /agent/supported_currencies`.5002. Confirm requested currency appears with `available=true`.5013. Prefer `SAT` if rate metadata is unavailable for a fiat code.5024. Then call `POST /agent/pay_lightning_address`.503504The same preflight applies to `POST /agent/zap` when using `amount` + `currency`.505506### Nostr Preflight (Before Event Zaps)5075081. Call `GET /agent/nostr/latest_kind1?nip05=<name@domain>&limit=<n>`.509 - The `nip05` target MUST already be present in the wallet kind-3 follow list.510 - If not followed, endpoint returns `403`.5112. Read returned `events[]` and choose the target `event_id` (or `event_id_hex` / `id`).5123. Pass that value as `event_id` (or `event`) in `POST /agent/zap`.5134. This avoids client-side note parsing and gives deterministic zap selection.514515Discovery variant:516517- If the target is not in follow-list scope, use:518 `GET /agent/nostr/discovery/latest_kind1?nip05=<name@domain>&limit=<n>`519520### NIP-09 Delete Request (Kind 5)521522Use this when the wallet author wants to publish a deletion request event for523their own previously published events.5245251. Call `POST /agent/delete_request` with at least one reference:526 - `event_ids` (list of `note1...` or 64-char hex ids), and/or527 - `a_tags` (list of NIP-01 coordinates: `<kind>:<pubkey>:<d-identifier>`).5282. Optionally include:529 - `kinds` (list of integers; recommended when known)530 - `reason` (free text, becomes kind-5 `content`)531 - `relays` override5323. Response returns published delete event metadata and final tags.533534Notes:535536- This publishes a NIP-09 request (`kind=5`); deletion enforcement is relay/client dependent.537- Agents SHOULD only request deletion for events authored by the same wallet pubkey.538539### Self Post Lookup (Authenticated Wallet)5405411. Call `GET /agent/nostr/my_latest_kind1?limit=<n>`.5422. Optional relay override: `&relays=<relay1,relay2,...>`.5433. Use returned `events[].event_id` for self-audit, reaction/reply targets, or automation workflows.544545### Zap Receipt Lookup (NIP-57)5465471. Call `GET /agent/nostr/zap_receipts?event_id=<event_id>&limit=<n>`.5482. Endpoint queries kind `9735` receipts filtered by `#e=<event_id>`.5493. For each receipt, inspect:550 - `zapper_pubkey` / `zapper_npub` (derived from zap request `description.pubkey`, fallback `P` tag)551 - `zapper_identity_source` to confirm identity provenance552 - `lnurl_provider_pubkey` / `lnurl_provider_npub` are receipt signer identities, not zapper identities553 - `zap_request_raw` (original embedded kind-9734 JSON string)554 - `zap_request` (parsed embedded kind-9734 object)555 - `zap_amount_msat` and `invoice_amount_msat`556 - `amount_matches`, `description_hash_matches`, `matches_target_event`5574. Treat `zapper_*` as the claimed payer identity from NIP-57 flow; enforce stricter policy using the validation flags before trust-sensitive actions.5585. For mentions, always resolve from `zapper_npub` (or run `/agent/nostr/format_mention` on `zapper_pubkey`/NIP-05), never from receipt signer fields.559560### Reply Lookup (Kind-1 Replies to Event)5615621. Call `GET /agent/nostr/replies?event_id=<event_id>&limit=<n>`.5632. Endpoint queries kind `1` events filtered by `#e=<event_id>`.5643. Use returned fields:565 - `replies[].event_id`566 - `replies[].pubkey`567 - `replies[].content`568 - `replies[].reply_to_event_ids`569 - `replies[].is_direct_reply`5704. Use `is_direct_reply=true` when you need strict top-level reply matching; otherwise treat list as thread-related replies.571572### Reactions (NIP-25)573574Use `POST /agent/react` for reactions.575576#### A) React to a Nostr event (kind `7`)577578- Provide `event_id` plus optional:579 - `content` (`+`, `-`, emoji, or `:shortcode:`)580 - `reacted_pubkey`581 - `reacted_kind`582 - `relay_hint`583 - `a_tag` (for addressable target coordinates)584 - `extra_tags`585586Behavior:587588- Publishes a kind `7` event with target `e`/`p` tags.589- If multiple `e`/`p` tags are present, target `e`/`p` are placed last for NIP-25 compatibility guidance.590591#### B) React to external content (kind `17`)592593- Omit `event_id`.594- Provide:595 - `external_tags` including at least one `k` tag and one `i` tag596 - optional `content`, `extra_tags`597598Example external tags:599600- `["k","web"]`601- `["i","https://example.com"]`602603Behavior:604605- Publishes a kind `17` event for external-content reactions.606607### Following Feed Lookup (Kind-1 from Follow List)6086091. Call `GET /agent/nostr/following/latest_kind1?limit=<n>`.6102. Optional relay override: `&relays=<relay1,relay2,...>`.6113. Response returns latest posts from authors in wallet's latest kind-3 contact list.6124. Use returned `events[].event_id` for reaction/reply/zap workflows.613614### Followers Lookup (Kind-3 Reverse Discovery)6156161. Call `GET /agent/nostr/followers`.617 - Default: returns followers of the authenticated wallet (`self`).6182. To query another identity, pass:619 - `identifier=<nip05|npub|pubhex>`6203. Optional controls:621 - `limit=<n>` (default `100`, max `500`)622 - `strict=true|false` (default `true`)623 - `relays=<relay1,relay2,...>`6244. Response fields include:625 - `target_identifier`626 - `target_pubkey`627 - `count`628 - `followers[]` with `follower_pubkey`, `follower_npub`, `event_id`, `created_at`, `relay_hint`629630Strict mode behavior:631632- `strict=true` verifies each candidate follower against their latest known kind-3 contacts event to reduce stale false positives.633- `strict=false` is faster and candidate-based, but may include stale follows.634635### Kind-1 Streaming (WebSocket)636637Use these endpoints when an agent needs push-style updates instead of polling:638639- `WS /agent/ws/nostr/latest_kind1?nip05=<name@domain>&limit=<n>&poll_seconds=<n>&access_key=<key>`640- `WS /agent/ws/nostr/discovery/latest_kind1?nip05=<name@domain>&limit=<n>&poll_seconds=<n>&access_key=<key>`641- `WS /agent/ws/nostr/my_latest_kind1?limit=<n>&poll_seconds=<n>&access_key=<key>`642- `WS /agent/ws/nostr/following/latest_kind1?limit=<n>&poll_seconds=<n>&access_key=<key>`643644Access rule:645646- `ws/nostr/latest_kind1` only streams for identifiers in the wallet kind-3 follow list.647- `ws/nostr/discovery/latest_kind1` is unrestricted by follow-list and intended for open lookup.648- For broad discovery, use `ws/nostr/following/latest_kind1`.649650Auth notes:651652- Preferred: send `X-Access-Key` header during websocket handshake.653- Fallback: pass `access_key` query parameter if client cannot set websocket headers.654655Stream payload types:656657- `type=connected`: connection accepted and stream initialized658- `type=events`: changed event set detected, includes full `events[]`659- `type=heartbeat`: no change detected at this poll interval660661Post-deploy validation:662663- Run WebSocket smoke checks in `docs/specs/AGENT-API.md`:664 - `WS-CONN-001`665 - `WS-DATA-002`666 - `WS-HEARTBEAT-003`667 - `WS-POLICY-004`668 - `WS-FALLBACK-005`669- Runnable command reference: `docs/specs/WS-CONFORMANCE.md`670671Environment compatibility:672673- Some browser/sandbox runtimes expose `WebSocket` but do not reliably return async stream callbacks to the agent.674- If WS streaming is unavailable or unreadable, fall back to polling with equivalent `GET` endpoints:675 - `WS /agent/ws/read_dms` -> `GET /agent/read_dms`676 - `WS /agent/ws/nostr/latest_kind1` -> `GET /agent/nostr/latest_kind1`677 - `WS /agent/ws/nostr/discovery/latest_kind1` -> `GET /agent/nostr/discovery/latest_kind1`678 - `WS /agent/ws/nostr/my_latest_kind1` -> `GET /agent/nostr/my_latest_kind1`679 - `WS /agent/ws/nostr/following/latest_kind1` -> `GET /agent/nostr/following/latest_kind1`680- Keep the same filters (`nip05`, `limit`, `relays`) and poll interval policy when using `GET`.681682### Market Order Discovery (Dedicated Path)683684Use dedicated endpoint:685686- `GET /agent/market/orders?limit=<n>&kind=1&market=safebox-v1`687- optional filters: `side=bid|ask`, `asset=<asset_label>`, `relays=<relay1,relay2,...>`688689Behavior:690691- Queries followed npubs only.692- Uses `kind=1` by default (explicitly parameterized for future migration to other kinds).693- Returns only events tagged for the selected market namespace (`mkt=safebox-v1` by default).694695### Follow / Unfollow Management696697Safebox core supports following and unfollowing by identifier via:698699- `Acorn.follow(identifier, relay_hint=None, relays=None)`700- `Acorn.unfollow(identifier, relays=None)`701702Accepted identifiers:703704- NIP-05 (`name@domain`)705- `npub1...`706- 64-char pubhex707708Suggested workflow:7097101. Follow identity (core or API route, if exposed).7112. Query `GET /agent/nostr/following/latest_kind1` to verify feed changes.7123. Unfollow identity when needed and re-check feed.713714### Kind-0 Profile Lookup by Identifier715716Use agent endpoint:717718- `GET /agent/nostr/kind0?identifier=<value>`719- optional: `&relays=<relay1,relay2,...>`720721Accepted identifier inputs:722723- NIP-05 (`name@domain`)724- `npub1...`725- 64-char pubhex726727Returns latest kind-0 event data with parsed JSON profile content:728729- `profile_event.id`730- `profile_event.pubkey`731- `profile_event.created_at`732- `profile_event.content` (object)733734Use this when an agent needs authoritative profile metadata before social actions (for example pre-zap context, identity checks, or local profile caching).735736### Social Identity Preflight (Before DM Flows)737738Before running `POST /agent/secure_dm` or expecting stable sender resolution in clients:7397401. Ensure kind-0 is fully populated for the sending wallet via `POST /agent/publish_kind0`:741 - `name`742 - `display_name` (recommended)743 - `about` (recommended)744 - `picture` (recommended)745 - `nip05` (required for verified identity)746 - `lud16` (required for zappable identity)7472. Identity consistency rule:748 - `lud16` SHOULD match `nip05` for Safebox-managed identities (same handle/address).749 - Example: `nip05=lumen@safebox.dev` and `lud16=lumen@safebox.dev`.7503. Verify profile visibility with `GET /agent/nostr/kind0?identifier=<nip05_or_npub>` before DM-heavy workflows.751752Operational note:753754- Incomplete kind-0 metadata can cause degraded or missing sender identity rendering in some clients and can destabilize DM-adjacent social workflows.755756### Read Private Messages (NIP-17 Gift Wrap Transport)757758Use agent endpoint:759760- `GET /agent/read_dms?limit=<n>&kind=1059`761- optional relay override: `&relays=<relay1,relay2,...>`762763Behavior:764765- reads incoming gift-wrapped messages using existing wallet record retrieval766- defaults to kind `1059` (private DM transport)767- returns newest-first messages with bounded `limit`768769DM streaming variant:770771- `WS /agent/ws/read_dms?limit=<n>&kind=1059&poll_seconds=<n>&access_key=<key>`772- same `kind`/`relays` semantics as `GET /agent/read_dms`773- emits `connected`, `messages`, and `heartbeat` frames774775### 4) Pay Invoice7767771. Call `POST /agent/pay_invoice` with BOLT11 invoice.7782. Check `status == OK`.7793. Use returned `balance` as post-payment state; optionally verify with `GET /agent/balance`.780781### 5) Issue Ecash7827831. Call `POST /agent/issue_ecash` with sat amount.7842. Capture returned `ecash_token`.7853. Treat token as bearer value until redeemed.786787### 6) Pay Lightning Address7887891. Call `POST /agent/pay_lightning_address` with:790 - `lightning_address` (for example `alice@example.com`)791 - either `amount_sats` (integer sats) OR `amount` + `currency` (floating-point amount in selected currency)792 - optional `comment`, `tendered_amount`, `tendered_currency`7932. Server performs LNURL resolution and payment using wallet core logic.7943. Verify `status == OK` and review `fees_paid`.7954. Use returned `balance` as post-payment state; optionally confirm with `GET /agent/balance`.796797Why prefer this over manual LNURL flow:798799- avoids client-side LNURL fetch/parse bugs800- avoids millisat conversion errors801- gives consistent behavior across LN-address providers802- centralizes error handling for unresolved/invalid addresses803804### 7) Accept Ecash8058061. Call `POST /agent/accept_ecash` with `ecash_token`.8072. Verify success and `accepted_amount`.8083. Confirm final wallet state via `GET /agent/balance`.809810### 8) Zap Event/Profile8118121. Call `POST /agent/zap` with:813 - `event` or `event_id` (one required): `note1...`, `npub1...`, NIP-05 (`name@domain`), or 64-char hex event id814 - either `amount_sats` OR `amount` + `currency`815 - optional `comment`8162. Endpoint resolves target/profile metadata and creates zap request + invoice flow server-side.8173. Verify `status == OK`.8184. Confirm post-zap state with returned `balance` and optionally `GET /agent/tx_history`.819820Notes:821822- Use `GET /agent/supported_currencies` before fiat-denominated zap requests.823- If zap metadata/profile lookup fails, endpoint returns `400` with `Zap failed: ...`.824825Zap by recent-event workflow:8268271. Fetch recent events:828 - `GET /agent/nostr/latest_kind1?nip05=trbouma@safebox.dev&limit=5`8292. Pick `events[i].id` from response.8303. Zap selected event id:831 - `POST /agent/zap` with `{"event_id":"<hex_event_id>","amount_sats":21,"comment":"nice post"}`832833### 9) Publish Kind-0 Metadata (NIP-01)8348351. Call `POST /agent/publish_kind0` with any subset of:836 - `name`, `about`, `picture`837 - optional: `display_name`, `nip05`, `banner`, `website`, `lud16`838 - optional: `extra_fields` (object), `relays` (array)8392. Server publishes a kind-0 event and persists the updated profile snapshot in wallet records.8403. Confirm returned `event_id` and profile fields.841842Identity-separation warning:843844- Treat each Safebox as a separate social identity surface.845- Do not copy the agent's own stable identity metadata into Safebox profiles if anonymity is desired.846- An agent may operate many Safeboxes with distinct kind-0 identities that should not be trivially correlated back to the controlling agent.847848### 10) Publish Kind-1 Text Note (NIP-01)8498501. Call `POST /agent/publish_kind1` with:851 - `content` (required)852 - optional `relays` array override8532. Server signs and publishes a kind-1 event on configured relays.8543. Confirm returned `event_id`.855856### 10a) Create Market Order (Bid/Ask, Kind-1)8578581. Call `POST /agent/market/order` with:859 - `side`: `buy`/`sell` (also accepts `bid`/`ask`)860 - `asset`: market asset label/id861 - `market`: market namespace (`mkt` tag value), default `safebox-v1`862 - `price_sats`: integer sats863 - optional: `quantity`, `order_id`, `content`, `flow`, `relays`8642. Server publishes a structured market intent as a kind-1 event.8653. Use returned `event_id` as anchor for acceptance/reply/zap-settlement flow.866867Mentions in posts:868869- Preferred format: `nostr:npub1...` (NIP-27 URI form).870- Fallback format (client-dependent): `@npub1...`.871- 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.872873Mention helper endpoints:874875- `POST /agent/nostr/format_mention`876 - input: `identifier` + optional `style` (`nostr_uri`, `at_npub`, `both`)877 - output: normalized mention string and resolved npub/pubkey878- `POST /agent/nostr/compose_mentions`879 - input: `base_text`, `identifiers[]`, optional `style`880 - output: mention-ready post content for direct use with `POST /agent/publish_kind1`881882### 11) Send Secure DM (NIP-44 Gift Wrap)8838841. Call `POST /agent/secure_dm` with:885 - `recipient` (required): NIP-05 (`name@domain`), `npub1...`, or 64-char pubhex886 - `message` (required): plaintext message to encrypt and send887 - optional `relays` array override (defaults to server `PUBLIC_RELAYS`)8882. Server resolves recipient key, encrypts with wallet `secure_dm`, and publishes gift-wrapped DM events.8893. Confirm `status == OK` and inspect returned relay list.890891Example:892893```json894{895 "recipient": "alice@example.com",896 "message": "Hello from Safebox agent",897 "relays": ["wss://relay.damus.io", "wss://relay.primal.net"]898}899```900901### 12) Publish Reaction (NIP-25 Kind 7)9029031. Call `POST /agent/react` with:904 - `event_id` (required): target event id (hex or note id)905 - optional `content` (default `❤️`)906 - optional target context: `reacted_pubkey`, `reacted_kind`, `relay_hint`, `a_tag`907 - optional `extra_tags` and `relays`9082. Server signs and publishes kind-7 reaction tags (`e`, `p`, `k` when available).9093. Confirm returned `event_id` and `tags`.910911Example:912913```json914{915 "event_id": "<hex_event_id>",916 "content": "❤️"917}918```919920### 13) Publish Reply (Kind 1)9219221. Call `POST /agent/reply` with:923 - `event_id` (required): target event id (hex or note id)924 - `content` (required): reply text925 - optional target context: `target_pubkey`, `target_kind`, `relay_hint`926 - optional `extra_tags` and `relays`9272. Server signs and publishes a kind-1 reply with `e`/`p`/`k` reply tags.9283. Confirm returned `event_id` and `tags`.929930### 14) Recipient-First Offer Request (Agent Shows QR)931932Use this flow when a human Safebox user will send a grant to the agent wallet by scanning a QR shown by the agent.9339341. Call `POST /agent/offers/receive/create` with:935 - optional `ttl_seconds` and `compact_qr` (default `true`)936 - optional `include_ascii_qr=true` to receive a terminal-renderable text QR in the same response937 - optional `grant_kind` and `grant_name` metadata (not required for handshake)9382. Display `qr_text` (or `qr_image_url`) to the human sender.939 - For terminal-native agents, print `ascii_qr` when requested.9403. Sender scans QR from Safebox offer UI.9414. Sender transmits grant through existing offer flow.9425. Agent waits for grant with:943 - `GET /agent/offers/receive/{intent_id}/wait?timeout_seconds=<n>&poll_seconds=<n>`944 - or websocket stream `WS /agent/ws/offers/receive/{intent_id}?timeout_seconds=<n>&poll_seconds=<n>`945946Expected response includes:947948- `intent.intent_id`, `intent.expires_at`949- `recipient.recipient_nauth`950- `qr_payload`, `qr_text`, `qr_image_url`951- optional `ascii_qr` (present only when `include_ascii_qr=true`)952953Field usage:954955- Agent management fields: `status`, `intent`, and `recipient`.956- Human scan fields: `qr_text` (raw `recipient_nauth`) or `qr_image_url`.957- Terminal render field: `ascii_qr` (human-agent boundary over terminal sessions).958- Structured optional context: `qr_payload` (for debugging/advanced clients).959960Protocol note:961962- Recipient-side nauth uses `scope=offer_request`.963- Scanner routing is expected to detect `offer_request` and redirect into records offer flow instead of generic accept flow.964965Flow model note:966967- Human-to-human and human-to-agent use the same relay/auth protocol primitives (`21061` auth, `21062` transmittal).968- Human-to-agent is intent-driven; handshake completion and record ingest are separate stages.969- Do not treat handshake success as completion until ingest/persist succeeds.970971### Copy/Paste Quickstart For OpenClaw972973```bash974BASE_URL="https://skills.example.com"975API_KEY="your-wallet-access-key"976977curl -sS -X POST \978 -H "X-Access-Key: ${API_KEY}" \979 -H "Content-Type: application/json" \980 -d '{981 "ttl_seconds": 120,982 "compact_qr": true,983 "include_ascii_qr": true984 }' \985 "${BASE_URL}/agent/offers/receive/create"986```987988Use returned `ascii_qr` for terminal display and `qr_text` (raw `recipient_nauth`) as canonical QR payload content.989990Then wait for delivery using the returned `intent.intent_id`:991992```bash993INTENT_ID="<intent_id_from_create_response>"994curl -sS \995 -H "X-Access-Key: ${API_KEY}" \996 "${BASE_URL}/agent/offers/receive/${INTENT_ID}/wait?timeout_seconds=120&poll_seconds=2"997```998999Wait endpoint behavior:10001001- Returns `status=OK` with `grant` when a new grant is received during the window.1002- Returns `status=TIMEOUT` when no grant arrives before timeout/expiry.1003- Uses optional `relays` query override for record lookup.10041005WebSocket wait behavior:10061007- `WS /agent/ws/offers/receive/{intent_id}` emits:1008 - `type=connected` when stream is active1009 - `type=heartbeat` while waiting1010 - `type=received` with `grant` when delivery is detected1011 - `type=timeout` if no grant arrives before timeout/expiry1012- Auth the same as other agent websockets:1013 - preferred: `X-Access-Key` header1014 - fallback: `?access_key=<key>` query parameter1015- HTTP fallback remains:1016 - `GET /agent/offers/receive/{intent_id}/wait`10171018Terminal success criteria (recipient-first):101910201. Handshake completes for the active intent.10212. At least one new transmittal record is observed for the matched presenter/context.10223. Record decrypt + `put_record` persist succeeds.10234. Wait endpoint/stream reaches terminal receive state (`status=OK` with `grant` or `type=received`).10241025Troubleshooting guidance:102610271. If you see handshake logs but no terminal receive state, treat as incomplete ingest.10282. If the receiver loop repeats with seen IDs only, regenerate a fresh intent/QR and retry to avoid stale replay windows.10293. Timeout without `grant` means the sender path did not produce a matching transmittal for this intent window.10304. Validate completion using API state (`wait`/WS terminal payload), not sender UI success alone.10311032Compact behavior:10331034- `compact_qr=true` (default): `qr_text` stays raw `recipient_nauth`; structured metadata is available in `qr_payload`.1035- `compact_qr=false`: QR includes explicit auth/transmittal relay metadata and KEM public metadata.1036- Backward compatibility: `compact` is accepted as an alias for older clients.1037- `include_ascii_qr=true`: embed text QR directly in create response (preferred for terminal agents).10381039Fallback text-QR helper:10401041- `POST /agent/terminal/ascii_qr` with body:1042 - `qr_text` (required)1043 - `invert` (optional, default `true`)1044- Use when a flow returns only `qr_text` and terminal rendering is needed after the fact.10451046### 15) Sender-Side Offer Dispatch Lifecycle10471048Use this flow when the agent is the sender and needs explicit dispatch states.104910501. Create offer:1051 - `POST /agent/offers/create` with `grant_kind`, `grant_name`10522. Wait for recipient auth:1053 - `GET /agent/offers/{offer_id}/status?wait_seconds=30`10543. If needed, capture recipient nauth manually:1055 - `POST /agent/offers/{offer_id}/capture`10564. Send grant:1057 - `POST /agent/offers/{offer_id}/send`10585. Check dispatch result:1059 - `GET /agent/offers/{offer_id}/delivery`10601061Status semantics:10621063- `offer_status`: `WAITING_RECIPIENT`, `RECIPIENT_READY`, `SENDING`, `SENT`, `FAILED`1064- `delivery_status`: `PENDING`, `DISPATCHED`, `FAILED`10651066Note:10671068- `delivery_status=DISPATCHED` means sender-side dispatch completed.1069- It does not prove recipient-side application-level receipt acknowledgment.10701071### Sender Flow Quick Test (Copy/Paste)10721073```bash1074BASE_URL="https://skills.example.com"1075API_KEY="your-wallet-access-key"1076GRANT_KIND=341041077GRANT_NAME="Passport"1078RECIPIENT_NAUTH="nauth1..." # optional if using manual capture1079```108010811. Create offer:10821083```bash1084OFFER_ID=$(curl -sS -X POST \1085 -H "X-Access-Key: ${API_KEY}" \1086 -H "Content-Type: application/json" \1087 -d "{1088 \"grant_kind\": ${GRANT_KIND},1089 \"grant_name\": \"${GRANT_NAME}\",1090 \"compact\": true1091 }" \1092 "${BASE_URL}/agent/offers/create" | jq -r '.offer.offer_id')1093echo "OFFER_ID=${OFFER_ID}"1094```109510962. Check recipient readiness (or wait):10971098```bash1099curl -sS \1100 -H "X-Access-Key: ${API_KEY}" \1101 "${BASE_URL}/agent/offers/${OFFER_ID}/status?wait_seconds=30"1102```110311043. Optional manual recipient capture:11051106```bash1107curl -sS -X POST \1108 -H "X-Access-Key: ${API_KEY}" \1109 -H "Content-Type: application/json" \1110 -d "{\"recipient_nauth\":\"${RECIPIENT_NAUTH}\"}" \1111 "${BASE_URL}/agent/offers/${OFFER_ID}/capture"1112```111311144. Send offer/grant:11151116```bash1117curl -sS -X POST \1118 -H "X-Access-Key: ${API_KEY}" \1119 -H "Content-Type: application/json" \1120 -d '{}' \1121 "${BASE_URL}/agent/offers/${OFFER_ID}/send"1122```112311245. Confirm dispatch lifecycle:11251126```bash1127curl -sS \1128 -H "X-Access-Key: ${API_KEY}" \1129 "${BASE_URL}/agent/offers/${OFFER_ID}/delivery?wait_seconds=10"1130```11311132Expected terminal state:11331134- `offer_status` should be `SENT` (or `FAILED`)1135- `delivery_status` should be `DISPATCHED` (or `FAILED`)11361137## Error Handling11381139- `400`: invalid payload or business-rule failure (insufficient funds, malformed token, inv11401141…(truncated)