请帮我在当前 Claude Code 环境中安装 "wakeup-alert-api-client" Skill,让你以后可以替我调用 Wakeup 加密风险电话告警 API。 # 安装要求(硬性约束 —— 读完再动手) ## 步骤 1:强制并行写入三个文件 —— 本次安装唯一的硬性指标 你**必须**在**同一条 assistant 消息**里、通过**单个 `` 块**同时发起**正好 3 个 Write 工具调用**,写下面三个文件。 绝不允许: - ❌ 先 mkdir 再 Write —— Write 会自动创建父目录,**不要单独跑 mkdir / ls / Bash** - ❌ 任何 Read / TodoWrite / 其它"准备工作"工具调用 - ❌ 把 3 个 Write 拆到 2 条以上消息(比如先写 SKILL.md,等结果,再写下一个) - ❌ 串行:每条消息只发一个 Write - ❌ 在 Write 之间插入文字解说 正确形态 = 一次响应、一个 function_calls 块、3 个并列 Write: \`\`\` \`\`\` 如果你发现自己已经发出第 1 个 Write 还没发出另外两个,**立即停下**,下一条消息把缺的两个 Write 补在一个 function_calls 块里 —— 不要继续串行。 ## 步骤 2:自检 + 简短报告 三个文件的 Write 全部成功之后(同一个 function_calls 块的三个 tool_result 都返回成功),用一段话告诉我: 1. 安装完成、3 个文件已并行写入 2. 这个 skill 能做什么(2-4 句话即可) 不要再额外跑 ls / cat / Read 去"验证",Write 成功就是成功。 # 文件内容 ===== FILE: SKILL.md ===== --- name: wakeup-alert-api-client description: "Use when an agent needs to integrate with the Wakeup crypto risk alert service: pay x402 to create alert rules, manage rules with the returned accessToken, recover a lost token via /login, test rules, or inspect alert events. Wakeup uses a payer-bound token model on X Layer." --- # Wakeup Alert API Client Wakeup is a crypto risk monitoring service. A wallet pays once via x402 on X Layer to create a rule; that payment binds the wallet as owner and returns a 365-day `accessToken` for every subsequent operation. The wallet is the identity — no signup. ## Endpoints at a glance | Endpoint | Auth | Notes | |---|---|---| | `GET /api/v1/health` | none | connectivity check | | `POST /api/v1/rules` | x402 paid | returns `{ rule, accessToken, expiresAt }` | | `POST /api/v1/login` | x402 (tiny) | rotates token, returns rule history | | `GET/PATCH/DELETE /api/v1/rules[/:id]` | Bearer | PATCH only accepts `name` / `status` / `actions` / `pollIntervalSeconds` | | `POST /api/v1/rules/:id/test` | Bearer + x402 | `{"send":true}` fires real call + marks `consumed` | | `GET /api/v1/events[/:id]` | Bearer | real-trigger evidence only | | `POST /api/v1/phone-authorizations/spug` | Bearer | server logs in to Spug, returns SMS authorization URL + QR code | A rule that fires for real is auto-archived (`status = "consumed"`). To change `type` / `target` / `condition`, create a new paid rule. Load references on demand: - `references/api-reference.md` — rule schema, per-type payloads, full curl examples - `references/x402-payment.md` — 402 flow, payment header decoding, USDT amounts ## Payer wallet resolution `x-payer-address` defaults to the **OKX Agentic Wallet's X Layer (chain 196) address**. Resolve in this order — do NOT ask the user upfront: 1. Run `onchainos wallet status` (silent). If `loggedIn: true`, run `onchainos wallet addresses --chain xlayer` and use `data.xlayer[0].address`. Proceed. 2. If not logged in, ask the user: "Use the OKX Agentic Wallet (recommended — I'll log you in via email OTP), or paste a different X Layer wallet address?" Only ask if step 1 fails. 3. Honor an explicit `0x...` address if the user provided one in the prompt — skip steps 1-2 in that case. The same Agentic Wallet then signs the x402 payment via the okx-agent-payments-protocol skill (TEE path). Never invent or hard-code a payer address. ## Phone number formatting `actions[].phone` currently supports only China mainland mobile numbers: exactly 11 digits matching `^1[3-9][0-9]{9}$`, for example `13800138000`. Do NOT add `+86`, do NOT reformat to E.164, and do NOT include spaces or dashes. If the user gives an unsupported phone format, ask them for an 11-digit China mainland mobile number before creating the rule. The only time you ask about phone format is when the user did not provide a phone number at all and no prior `configId` exists for this payer. ## First-time Spug phone authorization Wakeup phone calls are delivered through Spug. After a user successfully creates their first rule that includes an inline `actions[].phone`, request a server-generated authorization QR code: ```bash curl -X POST "$BASE_URL/api/v1/phone-authorizations/spug" \ -H "Authorization: Bearer $WAKEUP_TOKEN" ``` The Wakeup server, not the user's agent machine, logs in to Spug using server-side `SPUG_AUTH_USERNAME` / `SPUG_AUTH_PASSWORD`, reuses the cached `x-token`, fetches `https://push.spug.cc/mobile/channel/auth-code/`, builds `https://push.spug.cc/sms?body=`, and returns: ```json { "authCode": "183744", "smsUrl": "https://push.spug.cc/sms?body=183744", "qrCodeImageUrl": "https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=...", "qrCodeMarkdown": "电话告警规则已创建...![电话告警授权二维码](...)", "qrCodeDataUrl": "data:image/png;base64,...", "tokenCached": true, "tokenRefreshedAt": "2026-05-28T..." } ``` Show the returned `qrCodeMarkdown` to the user. It contains the Chinese prompt, a normal HTTPS QR image, the fallback `smsUrl`, and the auth code. Do not ask the user for Spug credentials. Do not run this endpoint for every rule; only do it when `POST /api/v1/rules` returns top-level `phoneAuthorizationRequired: true`, or when the user says phone delivery is unauthorized. If image/media rendering fails, still show `qrCodeImageUrl` and `smsUrl`. ## Workflow 1. Pick base URL (default `https://wakemeup.cc`, or user-provided). Optionally hit `GET /api/v1/health`. 2. **Create rule (paid)** — `POST /api/v1/rules` with `x-payer-address: `. Pay via x402 (see x402-payment.md). On 201, **persist `accessToken` immediately** to disk (see Token Persistence) before responding. 3. **First phone setup only** — if rule creation returns top-level `phoneAuthorizationRequired: true`, call `POST /api/v1/phone-authorizations/spug` before the final user-facing reply and show the returned QR code/link for user scanning. 4. **Manage rules (Bearer, free)** — list / get / patch / delete via `Authorization: Bearer wkp_...`. 5. **Test rule** — needs Bearer + x402. `{"send":false}` evaluates only; `{"send":true}` triggers phone delivery and archives the rule. 6. **Inspect activity** — first call `GET /api/v1/rules[/:id]` and read each rule's `executionLogs` (last 3 scheduler/manual checks, including non-triggered). Use `GET /api/v1/events` only for real triggers. When reporting rule status, render the last 3 execution log entries as a table with columns: `时间(UTC+8) | 来源 | 是否触发 | 消息`. 7. **Recover lost token** — `POST /api/v1/login` with `x-payer-address` (+ tiny x402). Previous token is invalidated; rewrite the token file with the new value. ## Token Persistence The agent has no cross-session memory. Persist immediately after `POST /api/v1/rules` or `POST /api/v1/login`. Path: `~/.wakeup/tokens.json` (chmod 600), keyed by base URL → lowercased payer address: ```json { "https://wakemeup.cc": { "0xabc...": { "accessToken": "wkp_...", "expiresAt": "2027-05-14T..." } } } ``` - Read this file before any manage / test / events workflow; match by base URL + lowercased payer address. - Write atomically (`tokens.json.tmp` then rename), mode `600`. - Overwrite on every successful create/login response — `/login` rotation invalidates the old value immediately. - If missing or expired (`expiresAt` in the past), tell the user and offer `/login` recovery. Never silently fail; never invent or reuse another wallet's token. After recovery, rewrite the file before continuing. - Never log the token, embed it in URLs, or send it to third parties. - If the user specifies a different storage location, honor it and skip the default path. ## Guardrails - `accessToken` is a bearer secret — never log, never embed in URLs, never share across wallets. - Don't request or store seller-side secrets (facilitator API keys, private keys). Those live on the server. - Exchange API keys for `cex_position_risk` must be read-only (no trade / withdrawal). - `dex_price` requires `source:"okx_dex"` + `chainIndex` + lowercase EVM `tokenContractAddress`. No CoinGecko `coinId` payloads. - Only `/rules`, `/rules/:id/test`, `/login` are x402-gated. No `/weather` endpoint here. - USDT (Tether USD) on X Layer uses 6 decimals: `amount:"10000"` = `0.01 USDT`; `/login` charges `amount:"10"` = `0.00001 USDT`. - If a paid call returns anything other than 402 before payment, surface the server error verbatim — never fabricate success or an accessToken. - A `consumed` rule is dead; create a fresh paid rule for continued monitoring. - Phone numbers currently support only 11-digit China mainland mobile numbers matching `^1[3-9][0-9]{9}$`; do not reformat or add country codes. - Spug credentials are server-side secrets. Never ask the user for them and never attempt local Spug login from the agent. ===== END FILE ===== ===== FILE: references/api-reference.md ===== # Wakeup API Reference Base URL: `https://wakemeup.cc` (or user-provided). Schema authoritative as of server commit on 2026-05-15 — sourced from `lib/rules/schemas.ts`, `lib/rules/types.ts`, `lib/rules/constants.ts`, and the evaluator code under `lib/sources/`. ## Endpoints ```text GET /api/v1/health free POST /api/v1/rules x402 → returns { rule, accessToken, expiresAt } POST /api/v1/login x402 (tiny) → rotates accessToken, returns rule history GET /api/v1/rules Bearer GET /api/v1/rules/:id Bearer PATCH /api/v1/rules/:id Bearer — only name/status/actions/pollIntervalSeconds DELETE /api/v1/rules/:id Bearer POST /api/v1/rules/:id/test Bearer + x402 GET /api/v1/events[?limit=50] Bearer GET /api/v1/events/:id Bearer POST /api/v1/phone-authorizations/spug Bearer → returns { authCode, smsUrl, qrCodeImageUrl, qrCodeMarkdown, qrCodeDataUrl, tokenCached, tokenRefreshedAt } ``` To change `type` / `target` / `condition`, create a new paid rule. Token persistence: see SKILL.md. `POST /api/v1/rules` includes top-level `phoneAuthorizationRequired`. It is `true` only when the server creates a new payer-scoped phone config from inline `actions[].phone`; agents MUST then call `POST /api/v1/phone-authorizations/spug` before the final user-facing reply and show the newly returned QR code/link. Existing `configId`, reused phone numbers, or omitted phone reuse return `false`. ## Rule Object (top-level) ```json { "name": "Human readable", "type": "", "target": { ... }, "condition": { ... }, "actions": [ { ... } ], "pollIntervalSeconds": 10 } ``` `type` is one of: `wallet_balance`, `cex_price`, `dex_price`, `aave_account_risk`, `cex_position_risk`. `pollIntervalSeconds` — integer; **min 3, max 86400, default 10** (constants in `lib/rules/constants.ts`). The Zod validator **silently strips unknown fields per type** — putting a field in `target` that the type doesn't recognize won't error at create time, but the scheduler won't read it. **Always cross-check against the per-type tables below.** ## Condition schema (shared across types) Defined in `lib/rules/schemas.ts:5-23`. The fields are: | Field | Type | Required when | Notes | |---|---|---|---| | `metric` | string | always | Valid set varies per type — see per-type tables | | `operator` | enum | always | `above` \| `below` \| `gte` \| `lte` \| `outside_range` \| `inside_range` | | `value` | number | always | Threshold (human-readable units, never minimal/wei) | | `upperValue` | number | when `operator ∈ {outside_range, inside_range}` | Must be `> value` | | `referenceValue` | number | when `metric == "absDiff"` | Anchor for absolute-diff comparisons | | `windowSeconds` | integer | when `metric == "changePct"` | Positive int; window for percentage change. **Belongs in `condition`, NOT in `target`.** | ## Actions schema Defined in `lib/rules/schemas.ts:25-36`, materialized in `lib/rules/service.ts:58-138`. ```json { "type": "phone_call", "configId": "...", "cooldownSeconds": 60 } ``` | Field | Type | Notes | |---|---|---| | `type` | string | Only `"phone_call"` is supported | | `configId` | string | Existing NotificationConfig id (mutually exclusive with `phone`) | | `phone` | string | Inline China mainland mobile number only; exactly 11 digits matching `^1[3-9][0-9]{9}$`, e.g. `13800138000` | | `cooldownSeconds` | int | Optional positive int | **Reconciliation logic at create time:** 1. If `configId` provided → must exist + belong to payer 2. Else if `phone` provided → server creates/looks up a NotificationConfig and stores `configId` 3. Else (neither) → server falls back to the **most-recent enabled** phone config for this payer (own → org-shared → recent-rule-derived). Errors if none found. Stored action always has `configId` (never raw `phone`). To rebind a phone, PATCH the rule with a new `actions` array. --- ## Per-Type Details ### `wallet_balance` `lib/rules/schemas.ts:48-58`, evaluator `lib/sources/wallet-balance.ts`. **Target:** | Field | Required | Notes | |---|---|---| | `chain` | yes | `ethereum` \| `arbitrum` \| `base` | | `walletAddress` | yes | EVM address | | `tokenContract` | no | ERC-20 contract; omit for native | | `tokenDecimals` | no | Auto-detected from contract if omitted | | `tokenSymbol` | no | Display label only | **Condition.metric**: must be `"balance"`. **Value is human-readable** (e.g. `1000` = 1000 USDC, not 1e9 micro-USDC). Evaluator returns `viem.formatUnits(raw, decimals)`. ```json { "type":"wallet_balance", "target":{ "chain":"ethereum", "walletAddress":"0x...", "tokenContract":"0xdAC17F958D2ee523a2206206994597C13D831ec7", "tokenSymbol":"USDT" }, "condition":{ "metric":"balance", "operator":"below", "value":30000000 } } ``` --- ### `cex_price` (Binance) `lib/rules/schemas.ts:60-69`, evaluator `lib/sources/price.ts`. **Target:** | Field | Required | Notes | |---|---|---| | `symbol` | yes | Binance symbol, e.g. `BTCUSDT`. Futures (`fapi.binance.com`) first, spot fallback | | `source` | no | Defaults to `"binance"` (only used to switch to `okx_dex` → use `dex_price` type instead) | | `symbolB` | conditional | Required when `metric == "ratio"` — second symbol for the numerator/denominator | **Condition.metric** (one of): | Metric | Extra required fields | What it observes | |---|---|---| | `price` | — | Current price | | `changePct` | **`condition.windowSeconds`** | `(currentPrice / pastPrice - 1) * 100`. Past price looked up from a 240-snapshot LRU in Redis; rule fails until a baseline ≥ `windowSeconds` old exists | | `ratio` | `target.symbolB` | `priceA / priceB` | | `absDiff` | `condition.referenceValue` | `currentPrice - referenceValue` | **Value units**: same units as price (USD-like, raw Binance quote). ```json // 5-min change percentage example — windowSeconds goes in CONDITION { "type":"cex_price", "target":{ "symbol":"BTCUSDT" }, "condition":{ "metric":"changePct", "operator":"below", "value":-3, "windowSeconds":300 } } ``` --- ### `dex_price` (OKX DEX Market) `lib/rules/schemas.ts:71-92`, evaluator `lib/sources/price.ts` (shares window/metric logic with `cex_price`). **Target:** | Field | Required | Notes | |---|---|---| | `source` | no | Defaults to `"okx_dex"` | | `symbol` | yes | Display label | | `chainIndex` | yes | OKX chain id (e.g. `"1"` ETH, `"56"` BSC) | | `tokenContractAddress` | yes | Lowercased EVM address; alias `tokenContract` accepted | | `chainIndexB` | conditional | For `metric == "ratio"` — defaults to `chainIndex` if omitted | | `tokenContractAddressB` | conditional | Required when `metric == "ratio"`; alias `tokenContractB` | | `symbolB` | no | Display label for second token | **Condition.metric**: same set as `cex_price` (`price` / `changePct` / `ratio` / `absDiff`), same `windowSeconds` / `referenceValue` rules. --- ### `aave_account_risk` `lib/rules/schemas.ts:94-97`, evaluator `lib/sources/aave.ts`. **Target:** | Field | Required | Notes | |---|---|---| | `chain` | yes | Only `ethereum` supported | | `walletAddress` | yes | EVM address | | `protocolDataProvider` | no | Override pool contract; defaults to mainnet Pool | **Condition.metric** (one of, from `getUserAccountData` return shape): | Metric | Range / units | |---|---| | `healthFactor` | 18-decimal scaled to float; **no debt → `~1.16e+59` (uint256 max / 1e18)** — never triggers `below` | | `totalCollateralUsd` | USD, 8-decimal precision | | `totalDebtUsd` | USD | | `availableBorrowsUsd` | USD | | `currentLiquidationThreshold` | 0-100 (percent) | | `ltv` | 0-100 (percent) | --- ### `cex_position_risk` `lib/rules/schemas.ts:99-134`, evaluators `lib/sources/cex-position.ts` (Hyperliquid) and `lib/sources/cex-account-position.ts` (Binance + OKX). **Target:** | Field | When | Notes | |---|---|---| | `exchange` | always | `hyperliquid` \| `binance` \| `okx` | | `walletAddress` | Hyperliquid only | Required for Hyperliquid; on-chain address | | `credentialId` | Binance/OKX | Reuse a stored credential. Mutually exclusive with inline keys | | `apiKey` + `apiSecret` | Binance/OKX, first time | Inline; server materializes to credential + rewrites target | | `apiPassphrase` | OKX only, first time | Required alongside `apiKey` + `apiSecret` | | `symbol` | optional | Omit → aggregate across **all positions**; provide → filter to one symbol | | `accountRef` | optional | Free-form label, passed through | **Inline-credentials materialization** (`lib/rules/service.ts:149-217`): when `apiKey/apiSecret/apiPassphrase` are inline, the server creates an `ExchangeCredential` row keyed by the payer wallet and rewrites the rule's `target` to `{exchange, credentialId, ...}`, stripping the secrets. Subsequent rules can omit credentials — server picks the **most-recent enabled** matching credential. **Condition.metric** (per exchange): | Metric | Hyperliquid | Binance | OKX | Definition | |---|---|---|---|---| | `positionCount` | ✓ | ✓ | ✓ | Open positions count | | `totalNotionalUsd` | ✓ | ✓ | ✓ | Σ `\|size × markPrice\|` | | `totalUnrealizedPnl` | ✓ | ✓ | ✓ | Σ unrealized PnL (USD) | | `nearestLiqDistancePct` / `distanceToLiqPct` | ✓ | ✓ | ✓ | Min `(markPrice - liqPrice) / markPrice * 100` over positions (longs); inverse for shorts | | `accountMarginRatio` | ✗ | ✓ | ✓ | `maintMargin / equity * 100`; `0` when no positions | Both `nearestLiqDistancePct` and `distanceToLiqPct` map to the same observation — they are aliases. **Unknown metric only errors at evaluation time** (`Unsupported CEX position metric`), not at create time — the create-time validator does not enumerate metrics per exchange. ```json { "type":"cex_position_risk", "target":{ "exchange":"okx" }, "condition":{ "metric":"accountMarginRatio", "operator":"above", "value":90 } } ``` --- ## curl Examples ```bash # Create (also requires x402; see x402-payment.md) curl -i -X POST "$BASE_URL/api/v1/rules" \ -H "Content-Type: application/json" \ -H "x-payer-address: 0xYourWallet" \ -H "PAYMENT-SIGNATURE: " \ -d '{ "name":"USDC<1000", "type":"wallet_balance", "target":{"chain":"ethereum","walletAddress":"0x...","tokenSymbol":"USDC"}, "condition":{"metric":"balance","operator":"below","value":1000}, "actions":[{"type":"phone_call","phone":"13800138000"}] }' # → 201 { "rule":{...}, "accessToken":"wkp_...", "expiresAt":"..." } # List / patch / delete (Bearer) curl "$BASE_URL/api/v1/rules" -H "Authorization: Bearer $WAKEUP_TOKEN" curl -X PATCH "$BASE_URL/api/v1/rules/$RULE_ID" \ -H "Authorization: Bearer $WAKEUP_TOKEN" -H "Content-Type: application/json" \ -d '{"status":"paused"}' curl -X DELETE "$BASE_URL/api/v1/rules/$RULE_ID" -H "Authorization: Bearer $WAKEUP_TOKEN" # Test (Bearer + x402); send:true triggers phone and marks rule consumed curl -X POST "$BASE_URL/api/v1/rules/$RULE_ID/test" \ -H "Authorization: Bearer $WAKEUP_TOKEN" \ -H "PAYMENT-SIGNATURE: " \ -H "Content-Type: application/json" -d '{"send":false}' # Login / token recovery (x402 tiny fee, rotates token, returns rule history) curl -X POST "$BASE_URL/api/v1/login" \ -H "x-payer-address: 0xYourWallet" \ -H "PAYMENT-SIGNATURE: " # Events (Bearer) curl "$BASE_URL/api/v1/events?limit=50" -H "Authorization: Bearer $WAKEUP_TOKEN" curl "$BASE_URL/api/v1/events/$EVENT_ID" -H "Authorization: Bearer $WAKEUP_TOKEN" # First-time Spug phone authorization QR (Bearer) curl -X POST "$BASE_URL/api/v1/phone-authorizations/spug" \ -H "Authorization: Bearer $WAKEUP_TOKEN" # → { "authCode":"183744", "smsUrl":"https://push.spug.cc/sms?body=183744", "qrCodeImageUrl":"https://api.qrserver.com/v1/create-qr-code/?...", "qrCodeMarkdown":"电话告警规则已创建...", "qrCodeDataUrl":"data:image/png;base64,...", "tokenCached":true, "tokenRefreshedAt":"..." } ``` ## ExecutionLogs shape Each `GET /api/v1/rules[/:id]` and `/login` response includes `executionLogs` (last 3 evaluation logs, scheduler + manual tests, including failures): ```json { "source": "scheduler", "status": "success", "triggered": false, "metric": "price", "observedValue": 100.12, "threshold": 90, "message": "...", "eventId": null, "createdAt": "2026-05-14T00:00:00.000Z" } ``` If `status == "failed"` with `observedValue: null`, the evaluator threw — typical causes: - `changePct` without `condition.windowSeconds`, or with windowSeconds in the wrong place (`target` instead of `condition`) - `cex_position_risk` with a metric the exchange doesn't support (e.g. `accountMarginRatio` on Hyperliquid) - `wallet_balance` with an invalid `tokenContract` or unreachable RPC ===== END FILE ===== ===== FILE: references/x402-payment.md ===== # x402 Payment Reference Protected endpoints: `POST /api/v1/rules`, `POST /api/v1/rules/:id/test`, `POST /api/v1/login`. Every other endpoint is Bearer or free. ## Payment terms Server prices in USD; facilitator settles in the canonical X Layer stablecoin (currently USDT, `USD₮0`, 6 decimals). Always read the live 402 challenge for the exact `asset` / `extra` / `amount`. ```text scheme: exact network: eip155:196 # X Layer price: $0.01 # /rules, /rules/:id/test (facilitator → ~10000 atomic USDT) loginFee: $0.00001 # /login proof-of-ownership (~10 atomic USDT) payTo: 0x69421fd0099582730233083da0ff79f33c865476 ``` ## Identifying the payer Until facilitator-derived payer wiring lands, send `x-payer-address: 0xYourWallet` on every paid call. Once facilitator settlement is wired in, this header becomes a fallback only. ## Inspect the 402 challenge Unpaid request returns `HTTP/1.1 402 Payment Required` plus a base64 `payment-required:` header. ```bash HEADER=$(curl -isS -X POST "$BASE_URL/api/v1/rules" \ -H "Content-Type: application/json" -H "x-payer-address: 0xYourWallet" -d '{}' \ | awk 'BEGIN{IGNORECASE=1}/^payment-required:/{print $2}' | tr -d '\r') node -e 'console.log(JSON.stringify(JSON.parse(Buffer.from(process.argv[1],"base64").toString()),null,2))' "$HEADER" ``` Decoded shape: ```json { "x402Version": 2, "accepts": [{ "scheme":"exact", "network":"eip155:196", "asset":"0x779ded0c9e1022225f8e0630b35a9b54be713736", "amount":"10000", "payTo":"0x69421fd0099582730233083da0ff79f33c865476", "extra":{ "name":"USD₮0", "version":"1" } }] } ``` ## Assemble the `PAYMENT-SIGNATURE` header (x402 v2 envelope) Header name is **`PAYMENT-SIGNATURE`** (not `X-PAYMENT` — that returns 402 silently here). Value is **base64(JSON)** of the envelope below. Sending raw JSON unencoded will be stripped by the HTTP layer (special chars in `,:"`) and the server falls back to "Payment required" as if no header was sent. ```jsonc { "x402Version": 2, "resource": , "accepted": , "payload": { "signature": "0x...", // from `onchainos payment pay` "authorization": { from, to, value, validAfter, validBefore, nonce } } } ``` Full example after signing: ```bash PAYLOAD='{"x402Version":2,"resource":{"url":"https://wakemeup.cc/api/v1/rules","description":"Create a crypto alert rule","mimeType":"application/json"},"accepted":{"scheme":"exact","network":"eip155:196","amount":"10000","asset":"0x779ded0c9e1022225f8e0630b35a9b54be713736","payTo":"0x69421fd0099582730233083da0ff79f33c865476","maxTimeoutSeconds":300,"extra":{"name":"USD₮0","version":"1"}},"payload":{"signature":"0x...","authorization":{"from":"0xPayer","to":"0x69421fd0099582730233083da0ff79f33c865476","value":"10000","validAfter":"0","validBefore":"1778835475","nonce":"0x..."}}}' SIG_HEADER=$(printf '%s' "$PAYLOAD" | base64 | tr -d '\n') curl -X POST "$BASE_URL/api/v1/rules" \ -H "x-payer-address: 0xPayer" \ -H "PAYMENT-SIGNATURE: $SIG_HEADER" \ -H "Content-Type: application/json" \ -d '' ``` ### Common 402 failure modes (replay-side) | Symptom | Likely cause | |---|---| | 402 with `error: "No matching payment requirements"` | Header **was** parsed but envelope shape is wrong — most often missing `resource`, or `accepted` was passed as an array instead of a single object, or top-level `scheme`/`network`/`payload` v1 shape was used instead of v2's `resource`/`accepted`/`payload` | | 402 with generic `error: "Payment required"` (same as unpaid) | Header **was not** parsed — wrong header name (`X-PAYMENT` instead of `PAYMENT-SIGNATURE`), or unencoded JSON got stripped by HTTP, or empty/malformed base64 | | 402 with `expired authorization` | `validBefore` already past — re-sign against a fresh 402, don't reuse the old signature | ## Paying client requirements The Wakeup server is the x402 seller. Caller needs its own x402 buyer SDK + funded X Layer wallet to retry with a valid `PAYMENT-SIGNATURE` header. If the agent lacks payment capability: 1. Send unpaid request with `x-payer-address`. 2. Decode and report the 402 challenge. 3. Ask for a payment-capable client or offline handling. 4. Never fabricate `accessToken` or claim success. After successful payment: - `POST /api/v1/rules` → `201 { rule, accessToken, expiresAt }`. Persist token. - `POST /api/v1/rules/:id/test` → eval/delivery JSON; rule marked `consumed` if `send:true` fired. - `POST /api/v1/login` → `200 { accessToken, expiresAt, rules }`. Previous token invalidated. ===== END FILE =====