# Hypawave Agent Instructions Hypawave is a non-custodial Bitcoin Lightning settlement protocol. Payment confirmation atomically releases encryption keys — "settlement IS authorization." Buyers pay creators directly via Lightning; Hypawave never holds principal funds. This file is the operating manual for AI agents using Hypawave over raw HTTP. For endpoint paths, request bodies, response shapes, and error codes, see the authoritative OpenAPI spec at https://hypawave.com/.well-known/openapi.json. This document covers *which* endpoints to call, in *what order*, and the protocol rules that JSON schema cannot express. ## Rules 1. **Preimage is mandatory for principal settlements.** Pay invoices only via a wallet or node that returns the payment preimage after settlement. Settlement proof is the authorization condition for release — verified by `SHA256(preimage) == payment_hash`. Without preimage, files and data will not unlock. - **Acceptable**: LND, Core Lightning, LNbits (admin key), Alby / NWC, Phoenixd, any programmable Lightning node. - **Not acceptable for automated confirmation**: Wallet of Satoshi, Phoenix mobile, generic consumer wallets. They pay invoices but do not expose preimage. - **Exception**: activation fees use Hypawave-issued bolt11s verified server-side via Hypawave's own receive wallet. The paying agent does not submit preimage for activation fees and may pay them from any Lightning wallet, including consumer wallets. 2. **Funds flow buyer→seller directly.** Hypawave never custodies principal funds. Activation fee bolt11s (small fees, Hypawave-issued) are the only invoices that route to Hypawave. All principal payments are creator-direct. 3. **Confirm before unlocking.** Always submit the preimage to the appropriate confirm endpoint before requesting file keys, download URLs, or receipts. Server-side state must transition to settled first. 4. **Honor `terms_hash`.** Path 3b offers carry a `terms_hash` over `{amount, currency, pricing_type, description, payment_destination, declared_price_sats}`. If terms have changed since you read them, the pay endpoint returns `409 terms_changed` — re-read and re-evaluate before paying. 5. **Base URL.** All endpoints resolve against `https://hypawave.com`. See `servers` in openapi.json. 6. **Do not invent.** If an endpoint, field, enum, header, or status code is not present in openapi.json, treat it as nonexistent. openapi.json is authoritative over this file in case of conflict. 7. **Handle errors precisely.** On any non-2xx response, read the JSON `error` field before retrying. Do not retry blindly. - `409 terms_changed` → re-read offer terms; do not retry the same `terms_hash`. - Expired invoice or offer → create a new payment request; do not retry. - Auth errors (`401`, `403`) → do not retry without fresh credentials, signature, or nonce. - `402` activation gating → wait for activation to settle, or call `/renew` if expired. 8. **Operator-supplied wallet.** Agents require an operator-supplied Lightning wallet credential capable of paying bolt11 invoices and returning preimage. Hypawave does not provision wallets. Activation fees (small, Hypawave-issued) may be paid from any wallet; principal settlements require a preimage-returning wallet per Rule 1. ## Choose Your Path | If your agent... | Use | |---|---| | Has (or will have) a human-owned Hypawave account with an API key | **Path 2** — Agent API (Bearer `sk_live_...` / `sk_test_...`) | | Has only a secp256k1 keypair, no account, and wants a single invoice | **Path 3a** — Accountless one-off invoice | | Has only a secp256k1 keypair, no account, and wants a reusable payment endpoint | **Path 3b** — Accountless persistent offer | Path 2 endpoints live under `/api/agent/*`. Paths 3a and 3b live under `/api/offers/*` and authenticate via pubkey signature headers. **Path 2 onboarding.** API keys (`sk_live_...` / `sk_test_...`) are issued only through the dashboard at https://hypawave.com — there is no agent-callable endpoint that mints API keys. A human operator must provision the key and supply it to the agent. For fully autonomous onboarding with no human in the loop, use Path 3a or 3b. ## Pubkey Signature Auth (Paths 3a / 3b) Signed routes under `/api/offers/*` require two signatures for body-bearing requests (one body-level + one header-level), and one signature for body-less requests (header-level only). Encoding choices below are mandatory — do not deviate. ### Encoding pins - **Curve**: secp256k1 (Bitcoin curve). - **Hash**: SHA-256. - **Pubkey**: 33-byte compressed, hex-encoded (66 chars). - **Signature**: DER-encoded ECDSA, hex-encoded (~140–144 chars), low-S enforced (BIP-62). Do not send compact 64-byte form. - **Timestamp**: seconds since Unix epoch as decimal string. Not milliseconds. - **Nonce**: unique random string, 8–128 chars, single-use per identity. Recommended: 16 random bytes hex-encoded (32 chars) — this is the tested convention used in Hypawave's reference scripts. - **Body serialization**: `JSON.stringify(body)` — insertion-order, no whitespace. The bytes you hash MUST equal the bytes you send as the HTTP body. ### Body-level signature (only when sending a body) For body-bearing requests like `POST /api/offers` and `POST /api/offers/create-invoice`, the body content itself is signed and stored permanently (used for `terms_hash` enforcement): 1. `terms_hash = sha256(JSON.stringify(body))`. 2. Sign `terms_hash` bytes with your private key → DER signature, hex-encoded. 3. Append to the body: `body.signed_payload_hash = terms_hash`, `body.signature = `. ### Header-level auth signature (every signed request) 1. `bodyStr = JSON.stringify(fullBody)` after appending body-level signature, or `""` for body-less requests. 2. `body_hash = sha256(bodyStr)`. 3. `timestamp = floor(now_unix_seconds).toString()`. 4. `nonce = random_16_bytes_hex`. 5. `canonical = body_hash + ":" + timestamp + ":" + nonce` — literal colon separators. 6. `canonical_hash = sha256(canonical)`. 7. Sign `canonical_hash` bytes with your private key → DER signature, hex-encoded. 8. Send headers: - `x-pubkey`: compressed pubkey hex - `x-signature`: DER hex of `canonical_hash` signature - `x-signed-payload-hash`: `body_hash` hex - `x-timestamp`: timestamp string - `x-nonce`: nonce hex ### Server enforcement - Timestamp tolerance: 300 seconds (5 min clock skew). - Each nonce is single-use per pubkey. - Identity is auto-created on first signed request from a new pubkey. ### Reference implementation (Node, @noble/secp256k1) Signature output MUST be DER-encoded hex. The server rejects compact 64-byte form. The helper below works across `@noble/secp256k1` v1.x (returns `Uint8Array` of DER) and v2.x (returns `Signature` object with `.toDERRawBytes()`). ```js import * as secp from "@noble/secp256k1"; import crypto from "crypto"; const sha256 = (s) => crypto.createHash("sha256").update(s).digest("hex"); // Version-tolerant: v1.x returns DER Uint8Array; v2.x returns Signature object. const toDerHex = (sig) => Buffer.from(sig.toDERRawBytes?.() ?? sig).toString("hex"); async function signRequest({ body, privKey }) { const privBytes = Buffer.from(privKey, "hex"); const pubKey = Buffer.from(secp.getPublicKey(privBytes, true)).toString("hex"); let fullBody = body; if (body) { const termsHash = sha256(JSON.stringify(body)); const termsSig = await secp.sign(Buffer.from(termsHash, "hex"), privBytes); fullBody = { ...body, signed_payload_hash: termsHash, signature: toDerHex(termsSig), }; } const bodyStr = fullBody ? JSON.stringify(fullBody) : ""; const bodyHash = sha256(bodyStr); const timestamp = Math.floor(Date.now() / 1000).toString(); const nonce = crypto.randomBytes(16).toString("hex"); const canonicalHash = sha256(`${bodyHash}:${timestamp}:${nonce}`); const sigBytes = await secp.sign(Buffer.from(canonicalHash, "hex"), privBytes); return { headers: { "Content-Type": "application/json", "x-pubkey": pubKey, "x-signature": toDerHex(sigBytes), "x-signed-payload-hash": bodyHash, "x-timestamp": timestamp, "x-nonce": nonce, }, body: bodyStr || undefined, }; } ``` ### Test vector — self-verify before hitting the API Inputs: - `priv_hex`: `0000000000000000000000000000000000000000000000000000000000000001` - `body` JSON: `{"amount":0.01,"pricing_type":"fiat","currency":"USD","description":"Test offer","payment_destination":"creator@example.com","activation_window":"30d"}` - `timestamp`: `"1779148800"` - `nonce`: `"a1b2c3d4e5f60718293a4b5c6d7e8f90"` Expected intermediate values: - `pub_hex` (compressed): `0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798` - `terms_hash`: `1e389e60df102bb8749776480eb933033f55bd5194d9f7767619523d12b46fd2` - `body_hash` (after appending body-level signature): `d2be4f9db4a7932489b52facf553852a34e1ff19a47147144b660ee5cbe6f5a5` - `canonical_hash`: `25b37371c291c101549b517f03530bb000294b734f051b420402e18d009e7a58` If `body_hash` and `canonical_hash` match, your hashing and JSON serialization are correct. Signatures vary by library (deterministic via RFC 6979 if your library implements it); the server validates by ECDSA verification, not byte-equality. **Compact-to-DER conversion**: if your library emits compact 64-byte signatures by default (common in `coincurve.PrivateKey.sign()`, some Rust bindings), convert to DER before sending. Hypawave's server only accepts DER. In Python `coincurve.PrivateKey.sign(msg)` returns DER, but `coincurve.PrivateKey.sign_recoverable()` and ad-hoc compact-mode calls do not — verify your specific call path. ### Recommended libraries - **JavaScript/TypeScript**: `@noble/secp256k1` or `@noble/curves` - **Python**: `coincurve` (libsecp256k1 bindings) or `python-bitcoinlib` - **Rust**: `secp256k1` crate (libsecp256k1 bindings) - **Go**: `github.com/btcsuite/btcd/btcec/v2` Use a battle-tested library. Do not implement secp256k1 primitives by hand. ## Recipes ### Recipe 1 — Agent as seller, Path 2 (managed) 1. `POST /api/agent/create-invoice` → returns `invoice_id`, `access_token`, `payment_url`, `instructions_url`. 2. (Optional) Attach encrypted files — see "File Attachment" below. 3. Forward the payment payload (including `access_token` and `instructions_url`) to the payer agent. 4. Wait for settlement confirmation. Settlement is observed via one of: (a) Payer-facing: payer POSTs `{payment_hash, preimage}` to `/api/invoice/{id}/confirm` (default DX path). (b) Server-to-server: trusted `wallet-webhook` delivers the proof from the payer's wallet provider. Then poll `GET /api/agent/list-invoices` or `GET /api/agent/receipt` for the settled state. 5. On settled, file keys become retrievable through the documented file-key endpoints. Fulfill any non-file unlock action server-side. ### Path 2 invoice options When calling `POST /api/agent/create-invoice`, the following options are available. Exact field names and defaults are authoritative in openapi.json. - **Currency**: pass any of 40+ supported fiat codes or `sats`. Server snapshots BTC conversion at creation time. Do not re-snapshot client-side. - **Expiry**: pass an expiry from the supported set (`"1h"`, `"24h"`, `"7d"`, `"never"`). Expired invoices reject settlement. Do not retry after expiry — create a new invoice. - **Execution webhook**: pass `execution_webhook` URL. Server fires `POST {execution_webhook}` on settlement with the settled invoice payload. Delivery is at-least-once — handlers must be idempotent. Do not block file-key release on webhook acknowledgment. - **Payment destination override**: server defaults `payment_destination` to the API key owner's `lightning_address`. To route principal elsewhere (marketplace, multi-wallet, pass-through), pass `payment_destination` in the request body. Never route principal through any Hypawave endpoint. ### Recipe 2 — Agent as buyer, Path 2 or Path 3a 1. Receive a payment payload containing `access_token` and `instructions_url`. 2. `GET /api/paystream-cb?token={access_token}` → returns `bolt11` and `terms_hash`. 3. Verify amount, destination, and any business-rule terms before paying. 4. Pay the bolt11 via your programmable wallet → obtain `preimage`. 5. `POST /api/invoice/{id}/confirm` with `{payment_hash, preimage, terms_hash?}`. 6. (For file delivery) `POST /api/get-invoice-files` with `{invoice_ids, token}`. 7. Per file: `GET /api/get-key?invoice_file_id=...&token={access_token}` → returns base64 `encryption_key` and hex `iv_hex`. 8. Per file: `POST /api/generate-download-url` with `{invoice_file_id, token}` → presigned URL (5 min). 9. Fetch encrypted blob, decrypt locally (AES-256-GCM). ### Recipe 3 — Agent as seller, Path 3a (accountless one-off invoice) 1. `POST /api/offers/create-invoice` (pubkey-signed) → returns invoice + `activation` sibling with `fee_bolt11`. 2. Pay the activation `fee_bolt11` from any Lightning wallet — preimage not required (Hypawave verifies its own receive invoice server-side). 3. (Optional) Attach a file: `POST /api/offers/upload-url` (pubkey-signed) → PUT encrypted blob to the returned presigned URL → `POST /api/offers/store-invoice-file` (pubkey-signed) → `POST /api/offers/invoice-file-key` (pubkey-signed). Exact request bodies are in openapi.json. 4. Forward the payment payload to the buyer. Buyer follows Recipe 2. ### Recipe 4 — Agent as seller, Path 3b (accountless persistent offer) 1. `POST /api/offers` (pubkey-signed) with `payment_destination`, `amount`, `currency`, optional `activation_window` (default `30d`, bounds `[1d, 365d]`), optional `max_payments` → returns offer + `activation` sibling with `fee_bolt11` and `terms_hash`. 2. Pay the activation `fee_bolt11` from any Lightning wallet — preimage not required (Hypawave verifies its own receive invoice server-side). 3. (Optional) Attach files: `POST /api/offers/upload-url` → PUT encrypted blob → `POST /api/offers/store-file` → `POST /api/offers/store-file-key` (all pubkey-signed). 4. Share the `offer_id` with buyers. Each pay-time call mints a fresh creator-direct bolt11. 5. After the activation window expires: `POST /api/offers/{id}/renew` to resume service. To deactivate: `DELETE /api/offers/{id}`. ### Recipe 5 — Agent as buyer, Path 3b 1. `GET /api/offers/{id}` (no auth) → read terms; capture `terms_hash`. 2. `POST /api/offers/{id}/pay` (no auth; activation gate enforced) → returns `bolt11` and `payer_secret`. 3. Pay the bolt11 creator-direct → obtain `preimage`. 4. `POST /api/offers/payment-intent/{id}/confirm` with `{preimage, payer_secret}`. 5. `GET /api/offers/payment-intent/{id}/status?secret={payer_secret}` → returns `claim_token` once settled. 6. (For file delivery) `GET /api/offers/payment-intent/{id}/file-key?claim_token={claim_token}` → returns wrapped key, `iv_hex`, `offer_file_id` per file. 7. Per file: `POST /api/offers/payment-intent/{id}/download-url` with `{offer_file_id, claim_token}` → presigned URL. 8. Fetch encrypted blob, decrypt locally (AES-256-GCM). ## File Attachment (Encryption Spec) All file attachments use AES-256-GCM. Hypawave never sees plaintext. Encrypt and decrypt client-side only. Do not transmit raw keys outside the documented register/retrieve calls. **Encrypt (creator).** Use these primitives exactly: ```js const key = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); const iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, key, fileBuffer ); const key_b64 = btoa(String.fromCharCode( ...new Uint8Array(await crypto.subtle.exportKey("raw", key)) )); const iv_hex = Array.from(iv) .map(b => b.toString(16).padStart(2, "0")).join(""); ``` **Register (creator).** Call the upload-url / store-file / store-file-key triplet for your path. Pattern is identical across paths: get presigned URL → PUT encrypted blob → register file metadata → register `key_b64` and `iv_hex`. Exact endpoint paths and request bodies are in openapi.json. Do not register a key before registering the file metadata. **Decrypt (payer).** Use these primitives exactly: ```js const key = await crypto.subtle.importKey( "raw", Uint8Array.from(atob(key_b64), c => c.charCodeAt(0)), { name: "AES-GCM" }, false, ["decrypt"] ); const iv = new Uint8Array( iv_hex.match(/.{2}/g).map(h => parseInt(h, 16)) ); const plaintext = await crypto.subtle.decrypt( { name: "AES-GCM", iv }, key, ciphertext ); ``` **Auth for file operations.** Use only the auth mode listed for your path. Do not mix: | Path | Creator (upload, register file, register key) | Payer (retrieve key, get download URL) | |---|---|---| | Path 2 | `Authorization: Bearer sk_live_...` / `sk_test_...` | `token` (access_token) in body or query | | Path 3a | Pubkey-signature headers | `token` (access_token) in body or query | | Path 3b | Pubkey-signature headers | `claim_token` or `preimage` | ## Fees - **Path 2 (postpaid)**: service fee deducted from creator balance on settlement. Settle outstanding fees via `POST /api/agent/topup` — server computes the amount; the agent must have an outbound-capable Lightning wallet to pay the returned bolt11. Developers can also settle in the dashboard. - **Paths 3a / 3b (upfront)**: fee = `max(min_fee_sats, floor(declared_amount_sats * fee_percent / 100))`. Paid before the invoice/offer becomes active. No refunds, no debt, no custody. Query `GET /api/public-settings` (no auth) for current `fee_percent`, `min_fee_sats`, invoice/file limits, and live BTC price. ## Self-Describing Payloads API responses include `instructions_url` pointing to this document so payer agents can discover the settlement flow with no prior Hypawave knowledge. Always forward `instructions_url` to downstream agents in your payment payload. ## Topology Constraints - Lightning is wallet-to-wallet. Hypawave is a coordination layer, not custody. - `payment_destination` is a Lightning Address or LNURL-pay URL. Creator-direct. - Activation fees route to Hypawave's managed receive wallet (LNbits); principal payments do not. - Path 3b queries `payment_destination` at each pay request, so the creator's address must be reachable when the buyer pays. ## Production Considerations ### Polling cadence - Poll status endpoints (e.g., `GET /api/agent/list-invoices`, `GET /api/agent/receipt`, `GET /api/offers/payment-intent/{id}/status`) at **2–3 second intervals**. - Do not poll faster than 1 second. You will hit the rate limit and trip backoff. - On any non-2xx response, apply exponential backoff before retrying. - Terminate polling on first of: settled status observed, invoice/offer expired, or your own attempt cap reached. ### Rate limits - **30 requests per 60-second window**, scoped per identity (Path 2 API key) or per pubkey (Path 3a / 3b). - Exceeding returns `429 rate_limit_exceeded`. Back off ~60 seconds before retrying. - `Retry-After` header is not currently sent. Assume a fixed 60-second reset. ### Webhook authenticity (`execution_webhook`) - `execution_webhook` POSTs are unsigned at the transport layer. No HMAC, no shared secret. - Verify every delivery cryptographically: `SHA256(payload.preimage) == payload.payment_hash` AND `payload.invoice_id` matches an invoice you own. Reject deliveries that fail either check. - Deliveries are fire-and-forget with a 5-second timeout. Respond fast — `200` with an empty body is sufficient. - Hypawave marks `execution_webhook_fired = true` after a successful POST and does not retry. If your handler fails mid-process, reconcile via polling (preimage is also retrievable from `GET /api/agent/receipt` or `GET /api/offers/payment-intent/{id}/receipt`). ### Expiry model Three distinct expiry concepts exist; the count and naming differ per path. Track each independently. | Path | Window 1 | Window 2 | Window 3 | |---|---|---|---| | Path 2 | `expires_in` (`"1h"` / `"24h"` / `"7d"` / `null`) — invoice lifetime | Principal bolt11 expiry (set by creator's wallet provider at fetch time) | — | | Path 3a | `expires_in` — invoice lifetime | `activation.expires_at` — activation fee bolt11 expiry (LNbits-set) | Principal bolt11 expiry (same as Path 2) | | Path 3b | `activation_window` (default `"30d"`, bounds `[1d, 365d]`) — offer payability window | `activation.expires_at` — activation fee bolt11 expiry (LNbits-set) | Principal bolt11 expiry (same as Path 2) | **Key rules:** - **Path 3b's `window_end` is stamped at activation-fee settlement, not at offer creation.** The response after settlement returns `window_end = settled_at + activation_window`. Plan renewals from this timestamp, not from offer creation time. - **Principal bolt11 expiry is not Hypawave-controlled.** It is set by the creator's wallet provider when the bolt11 is minted at pay time. Typically ~1 hour, but varies by provider. If expired before payment, refetch via `GET /api/paystream-cb` (Path 2 / 3a) or `POST /api/offers/{id}/pay` (Path 3b). The parent invoice/offer remains valid. - **Activation fee bolt11 expiry**: read `activation.expires_at` from the response. If unpaid by this timestamp, the activation row is lazy-expired and the parent is inert. Path 3a: re-create the invoice. Path 3b: call `POST /api/offers/{id}/renew` to mint a fresh activation bolt11. - **Path 3b renewal trigger**: when `POST /api/offers/{id}/pay` returns `402 offer_inactive`, call `/renew`. While the current window is still active, `/renew` returns `400 activation_not_needed` with the live `window_end` so you can schedule a future renewal. - **Path 2 / 3a invoice expiry**: when `expires_in` elapses, the invoice is permanently closed. Do not retry — create a new invoice. ## Reference - **OpenAPI spec** (endpoint shapes, error codes, authoritative): https://hypawave.com/.well-known/openapi.json - **SDK** (Path 2 only, TypeScript): `npm install @hypawave/sdk` - **Site**: https://hypawave.com - **Docs**: https://hypawave.com/docs - **Architecture**: https://hypawave.com/architecture - **FAQ**: https://hypawave.com/faq - **Privacy policy**: https://hypawave.com/privacy - **Terms of service**: https://hypawave.com/terms