{"openapi":"3.1.0","info":{"title":"OceanAlt — compliance layer for agent payments","version":"1.0.0","summary":"Before your agent pays, call OceanAlt once.","description":"Free, no-signup, CORS-enabled compliance screening and payment decisions for autonomous agents.\n\nEvery verdict comes with verifiable evidence — which sanctions list, which issuer freeze, which on-chain path — so an agent (or its auditor) can check the reasoning rather than trust a score.\n\nCoverage: 7 EVM chains (ethereum, base, bsc, polygon, arbitrum, optimism, avalanche) + TRON + Bitcoin + Solana.\nThe payment gateway runs 11 gates before settlement: Attribution (KYA) → Revocation check → Identity proof (anti-impersonation) → KYC gate (optional) → Per-payment limit → Daily cumulative limit → Payee allowlist → Mandate-intent match (anti prompt-injection) → AML sanctions & risk screening → Behavioral anomaly detection (optional) → Replay & double-spend prevention.\n\nLanguage: human-readable fields default to English. Pass `lang=zh` for Chinese. `signal_keys` is language-independent — branch on it.\n\nLimits of the claim: compliance screening is not full or licensed AML compliance, and a `clear` / `allow` result is not proof of safety — absence of evidence is not evidence of absence. Enforce your own mandate regardless.\n\nAPI keys: optional and only ever raise limits (POST /api/access-keys). A key's holder may additionally restrict it — scopes (screen_address, screen_endpoint, decide, payee_decide, batch_screen, trace_funds, evidence_bundle, intent_check, baseline_lookup), a source-IP allowlist, and an expiry — in which case a call outside those limits is refused 403 with reason_code key_scope_denied or key_ip_denied, and a call with an expired key is served at the anonymous level with key_rejected: expired. Keys with no restrictions set, and calls with no key at all, are unaffected. See /api/access-keys/settings.\n\nIdempotency: the POST /api/pay payment call and `rotate` accept an `Idempotency-Key` header (semantics per draft-ietf-httpapi-idempotency-key-header). Same credential + same key + same JSON body within 24 hours returns the original status and body with `Idempotent-Replayed: true`; nothing runs twice and the retry is not recorded as a replay attack. Keep the same nonce when retrying. Calls without the header are unchanged.\n\nWebhooks: notifications to a URL configured in the console are signed per Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature = v1,base64 HMAC-SHA256). Reject timestamps more than 300 seconds away. Failed deliveries are retried up to 5 attempts with the same webhook-id. See the top-level `webhooks` section.","contact":{"name":"OceanAlt","url":"https://oceanalt.com"},"license":{"name":"Free tier — fair use","url":"https://oceanalt.com/en/api-docs"}},"servers":[{"url":"https://oceanalt.com"}],"tags":[{"name":"free","description":"No API key, no signup. Fair-use rate limited."},{"name":"paid (x402)","description":"Pay-per-call over x402. Unpaid requests return 402 with a machine-readable quote."}],"paths":{"/api/decide":{"get":{"operationId":"decide_get","summary":"Decide before paying","description":"Your agent is about to pay — ask first. Returns a machine-executable decision allow / review / decline + verifiable evidence + retry semantics; allow/review also carry a verifiable settlement attestation to bind to your payment. Free, no key. Paid variant that also settles: /api/x402/decision.\n\nExample:\ncurl \"https://oceanalt.com/api/decide?to=0x8589427373D6D84E98730D7795D8f6f8731FDA16\"","tags":["free"],"parameters":[{"name":"to","in":"query","required":true,"description":"Payee address you're about to pay: 0x (EVM) / T (TRON)","schema":{"type":"string"}},{"name":"amountUsdc","in":"query","required":false,"description":"Amount in USDC (optional, recorded only)","schema":{"type":"number"}},{"name":"purpose","in":"query","required":false,"description":"Purpose/memo (optional)","schema":{"type":"string"}},{"name":"network","in":"query","required":false,"description":"EVM chain for 0x addresses (same as /api/risk)","schema":{"type":"string"}},{"name":"task","in":"query","required":false,"description":"Payment intent: what this payment is for (optional; sending any intent field opts into the intent check, and an empty intent is itself a finding)","schema":{"type":"string"}},{"name":"reasoning","in":"query","required":false,"description":"Payment intent: why this payee (optional)","schema":{"type":"string"}},{"name":"maxAmountUsdc","in":"query","required":false,"description":"Your declared per-payment cap in USDC (optional); amountUsdc above it → decline","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionResult"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `decide`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}},"post":{"operationId":"decide_post","summary":"Decide before paying","description":"Your agent is about to pay — ask first. Returns a machine-executable decision allow / review / decline + verifiable evidence + retry semantics; allow/review also carry a verifiable settlement attestation to bind to your payment. Free, no key. Paid variant that also settles: /api/x402/decision.\n\nExample:\ncurl \"https://oceanalt.com/api/decide?to=0x8589427373D6D84E98730D7795D8f6f8731FDA16\"","tags":["free"],"parameters":[{"name":"to","in":"query","required":true,"description":"Payee address you're about to pay: 0x (EVM) / T (TRON)","schema":{"type":"string"}},{"name":"amountUsdc","in":"query","required":false,"description":"Amount in USDC (optional, recorded only)","schema":{"type":"number"}},{"name":"purpose","in":"query","required":false,"description":"Purpose/memo (optional)","schema":{"type":"string"}},{"name":"network","in":"query","required":false,"description":"EVM chain for 0x addresses (same as /api/risk)","schema":{"type":"string"}},{"name":"task","in":"query","required":false,"description":"Payment intent: what this payment is for (optional; sending any intent field opts into the intent check, and an empty intent is itself a finding)","schema":{"type":"string"}},{"name":"reasoning","in":"query","required":false,"description":"Payment intent: why this payee (optional)","schema":{"type":"string"}},{"name":"maxAmountUsdc","in":"query","required":false,"description":"Your declared per-payment cap in USDC (optional); amountUsdc above it → decline","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionResult"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `decide`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/payee/decide":{"get":{"operationId":"payee_decide_get","summary":"The payee decides too","description":"The other half: before settling, the paying agent asks whether the payee accepts this payment. Returns accept / decline / hold_request_info / not_ready + reason codes + requested_fields + evidence + signature. All rules: payee readiness from the registry ladder, payer address screened in reverse, amount / purpose / KYA requirements from the payee's own policy. Free, no key.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/payee/decide\" -H \"content-type: application/json\" -d '{\"payee\":\"oceanalt-200lab-premium\",\"payer\":{\"address\":\"0x8589427373D6D84E98730D7795D8f6f8731FDA16\"},\"amountUsdc\":5,\"purpose\":\"api-call\"}'","tags":["free"],"parameters":[{"name":"payee","in":"query","required":true,"description":"Payee sid from the registry (see /registry.json)","schema":{"type":"string"}},{"name":"payer.address","in":"query","required":false,"description":"Payer address (screened in reverse)","schema":{"type":"string"}},{"name":"payer.agentId","in":"query","required":false,"description":"Payer agent's KYA id (required when the payee requires KYA)","schema":{"type":"string"}},{"name":"amountUsdc","in":"query","required":false,"description":"Amount (required when the payee sets limits)","schema":{"type":"number"}},{"name":"purpose","in":"query","required":false,"description":"Purpose (required when the payee whitelists purposes)","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `payee_decide`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}},"post":{"operationId":"payee_decide_post","summary":"The payee decides too","description":"The other half: before settling, the paying agent asks whether the payee accepts this payment. Returns accept / decline / hold_request_info / not_ready + reason codes + requested_fields + evidence + signature. All rules: payee readiness from the registry ladder, payer address screened in reverse, amount / purpose / KYA requirements from the payee's own policy. Free, no key.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/payee/decide\" -H \"content-type: application/json\" -d '{\"payee\":\"oceanalt-200lab-premium\",\"payer\":{\"address\":\"0x8589427373D6D84E98730D7795D8f6f8731FDA16\"},\"amountUsdc\":5,\"purpose\":\"api-call\"}'","tags":["free"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"payee":{"type":"string","description":"Payee sid from the registry (see /registry.json)"},"payer.address":{"type":"string","description":"Payer address (screened in reverse)"},"payer.agentId":{"type":"string","description":"Payer agent's KYA id (required when the payee requires KYA)"},"amountUsdc":{"type":"number","description":"Amount (required when the payee sets limits)"},"purpose":{"type":"string","description":"Purpose (required when the payee whitelists purposes)"}},"required":["payee"]}}}},"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `payee_decide`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/risk":{"get":{"operationId":"risk_get","summary":"Single address","description":"Screen one payee before paying: sanctions / mixer / our risk list, or high-risk on-chain signals? Returns a verdict plus verifiable evidence.\n\nExample:\ncurl \"https://oceanalt.com/api/risk?addr=0x8589427373D6D84E98730D7795D8f6f8731FDA16&network=ethereum\"","tags":["free"],"parameters":[{"name":"addr","in":"query","required":true,"description":"Address to screen: 0x (EVM) / T (TRON) / Solana base58","schema":{"type":"string"}},{"name":"network","in":"query","required":false,"description":"EVM chain for 0x addresses: ethereum (default) / base / bsc / polygon / arbitrum / optimism / avalanche","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScreenResult"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `screen_address`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/risk/batch":{"post":{"operationId":"batch_post","summary":"Batch screening","description":"Screen many addresses at once (deduped, up to 25). For OTC desks / batch jobs. Sorted risky → caution → clear.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/risk/batch\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"addresses\":[\"0x8589...\",\"TWd4...\",\"T...\"]}'","tags":["free"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"addresses":{"type":"string","description":"Array of addresses (JSON body), up to 25"}},"required":["addresses"]}}}},"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `batch_screen`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/risk/flow":{"get":{"operationId":"flow_get","summary":"Fund-flow (Tron + EVM)","description":"For a Tron USDT address: top-8 counterparties by amount, first USDT inflow, Tether-frozen counterparties, and bridge detection. EVM (ethereum/base/polygon/arbitrum/optimism, pass network) returns USDT/USDC counterparties + bridge detection.\n\nExample:\ncurl \"https://oceanalt.com/api/risk/flow?addr=TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb\"","tags":["free"],"parameters":[{"name":"addr","in":"query","required":true,"description":"Tron address (T…) or 0x EVM address","schema":{"type":"string"}},{"name":"network","in":"query","required":false,"description":"EVM chain for 0x (ethereum default / base / polygon / arbitrum / optimism); omit for Tron","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `trace_funds`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/endpoint":{"get":{"operationId":"endpoint_get","summary":"Payment endpoint screening","description":"Screen a URL or domain against public phishing databases plus hostname-shape signals. In x402 the agent gets the URL first and the payee address second; a spoofed endpoint hands back a fresh address no list has seen — only this layer catches it. Free, no key. clear means no match, not safe.\n\nExample:\ncurl \"https://oceanalt.com/api/endpoint?url=https://pay.example.com/x402\"","tags":["free"],"parameters":[{"name":"url","in":"query","required":true,"description":"URL or domain","schema":{"type":"string"}},{"name":"lang","in":"query","required":false,"description":"zh / en","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `screen_endpoint`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/intent/check":{"post":{"operationId":"intent_check_post","summary":"Intent check (anti blind-signing)","description":"Decode what calldata will actually do and compare it with the payment the agent believes it is making — approve(attacker, unlimited) dressed as a payment. match / mismatch / unknown; undecodable is not the same as safe. EVM: pass calldata; Solana: pass the base64 transaction (SPL Transfer / Approve / SetAuthority / CloseAccount are recognised).\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/intent/check\" -H \"content-type: application/json\" -d '{\"intent\":{\"action\":\"pay\",\"to\":\"0x1111111111111111111111111111111111111111\",\"amount\":\"10000000\"},\"contract\":\"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913\",\"data\":\"0x095ea7b3…\"}'","tags":["free"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"intent":{"type":"string","description":"{ action: 'pay', to, amount?, asset? }"},"contract":{"type":"string","description":"The transaction's to (token contract or recipient)"},"data":{"type":"string","description":"0x-prefixed calldata"},"value":{"type":"string","description":"Native value in wei"}},"required":["intent","contract","data"]}}}},"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `intent_check`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/x402/verify-requirements":{"post":{"operationId":"x402_verify_requirements_post","summary":"Verify signed x402 payment requirements","description":"A 402 is plaintext; any hop can swap payTo. Every OceanAlt 402 carries an Ed25519 signature in the PAYMENT-REQUIRED payload (extensions.signedRequirements). Pass the decoded JSON or the raw base64 to verify. Prefer verifying offline with the key at /.well-known/oceanalt-keys.json.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/x402/verify-requirements\" -H \"content-type: application/json\" -d '{\"body\":\"<base64 PAYMENT-REQUIRED>\",\"expect\":{\"payTo\":\"0x…\"}}'","tags":["paid (x402)"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","description":"Decoded PAYMENT-REQUIRED JSON or its raw base64"},"expect":{"type":"string","description":"{ payTo?, amount? } what you are about to pay"}},"required":["body"]}}}},"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"402":{"description":"Payment required — the `payment-required` header carries the x402 v2 quote (accepts[])."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/probe/plan":{"post":{"operationId":"probe_post","summary":"Probe payment (small amount first)","description":"Before a large payment, pay a minimum amount and verify before releasing the rest. The plan pins the address + chain + asset + endpoint fingerprint; POST /api/probe/verify compares it with what the caller reports now — this catches 'swap the payee after the probe succeeded'. OceanAlt never touches funds.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/probe/plan\" -H \"content-type: application/json\" -d '{\"to\":\"0x…\",\"amount\":\"500\",\"network\":\"eip155:8453\",\"asset\":\"USDC\"}'","tags":["free"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","description":"Payee address"},"amount":{"type":"string","description":"Full amount"},"network / asset / endpoint":{"type":"string","description":"Rest of the fingerprint"}},"required":["to","amount"]}}}},"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/fulfillment":{"get":{"operationId":"fulfillment_get","summary":"Fulfillment receipts (no custody)","description":"Paid, and delivered? The payer files 'I paid this tx (verified on-chain) and did / did not receive'; the payee files 'delivered / disputed'. Both statements are kept side by side; OceanAlt adjudicates nothing and holds no funds; per-payee rates only after 10 verified receipts. GET ?payee= for stats.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/fulfillment\" -H \"content-type: application/json\" -d '{\"role\":\"payer\",\"txHash\":\"0x…\",\"chain\":\"base\",\"payee\":\"0x…\",\"claim\":\"delivered\"}'","tags":["free"],"parameters":[{"name":"role","in":"query","required":true,"description":"payer | payee","schema":{"type":"string"}},{"name":"txHash / chain / payee","in":"query","required":true,"description":"Required when the payer files","schema":{"type":"string"}},{"name":"claim","in":"query","required":false,"description":"delivered | not_delivered (payer) · delivered | disputed (payee)","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}},"post":{"operationId":"fulfillment_post","summary":"Fulfillment receipts (no custody)","description":"Paid, and delivered? The payer files 'I paid this tx (verified on-chain) and did / did not receive'; the payee files 'delivered / disputed'. Both statements are kept side by side; OceanAlt adjudicates nothing and holds no funds; per-payee rates only after 10 verified receipts. GET ?payee= for stats.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/fulfillment\" -H \"content-type: application/json\" -d '{\"role\":\"payer\",\"txHash\":\"0x…\",\"chain\":\"base\",\"payee\":\"0x…\",\"claim\":\"delivered\"}'","tags":["free"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","description":"payer | payee"},"txHash / chain / payee":{"type":"string","description":"Required when the payer files"},"claim":{"type":"string","description":"delivered | not_delivered (payer) · delivered | disputed (payee)"}},"required":["role","txHash / chain / payee"]}}}},"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/evidence/bundle":{"get":{"operationId":"evidence_bundle_get","summary":"Evidence bundle (signed, verifiable offline)","description":"Everything OceanAlt can say about an address in one signed bundle: screening verdict + evidence, self-built profile signals, one-hop relations to listed addresses, fulfillment receipt stats. Anyone may request it, free, no identity required; it contains only what is already public, never user data. OceanAlt is a witness, not a plaintiff — no conclusions, no attribution of persons.\n\nExample:\ncurl \"https://oceanalt.com/api/evidence/bundle?address=0x…&network=ethereum\"","tags":["free"],"parameters":[{"name":"address","in":"query","required":true,"description":"Address","schema":{"type":"string"}},{"name":"network","in":"query","required":false,"description":"Network","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"403":{"description":"An API key was sent and its holder's restrictions do not allow this call: reason_code `key_scope_denied` (this endpoint is scope `evidence_bundle`, which the key is not limited to) or `key_ip_denied` (the call did not come from the key's IP allowlist). Calling without a key is unaffected."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/observatory/index":{"get":{"operationId":"observatory_get","summary":"Observatory index series","description":"Narrative density index: a term's weekly density (hits per 1,000 items) and index (base = 100) within our corpus. Every point carries numerator / denominator, CSV available, and each response includes a corpus block with definitions. Siblings: /bubbles (all terms for a week), /emerging (fastest-growing), /term/{slug} (discovery panel), /export (raw data).\n\nExample:\ncurl \"https://oceanalt.com/api/observatory/index?term=x402&from=2026-W28&to=2026-W36\"","tags":["free"],"parameters":[{"name":"term","in":"query","required":true,"description":"Term slug, see /api/observatory/bubbles","schema":{"type":"string"}},{"name":"from / to","in":"query","required":false,"description":"ISO weeks like 2026-W28; default last 12 weeks","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"csv for download","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/pay":{"get":{"operationId":"pay_manifest_get","summary":"Gateway self-description","description":"Returns the gateway's gate list, auth model, rate limits and usage. An agent can discover how to integrate from this alone.\n\nExample:\ncurl \"https://oceanalt.com/api/pay\"","tags":["free"],"parameters":[{"name":"lang","in":"query","required":false,"description":"en (default) or zh","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}},"post":{"operationId":"pay_preview_post","summary":"Stateless pre-flight check","description":"No settlement, no nonce spent, no credential needed: asks 'would this payment pass the gates?' and returns each gate's result. The recommended first call in an x402 flow.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/pay\" -H \"content-type: application/json\"   -d '{\"action\":\"preview\",\"amountUsdc\":25,\"to\":\"0x8589427373D6D84E98730D7795D8f6f8731FDA16\"}'","tags":["free"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","description":"literal \"preview\""},"amountUsdc":{"type":"number","description":"Amount in USDC"},"to":{"type":"string","description":"Payee address"},"purpose":{"type":"string","description":"Purpose"},"agentId":{"type":"string","description":"Registered agent (its mandate is checked if given)"},"network":{"type":"string","description":"Chain: base / solana / ethereum …"},"lang":{"type":"string","description":"en (default) or zh"}},"required":["action","amountUsdc","to"]}}}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid input, or a malformed Idempotency-Key (reason_code idempotency_key_invalid)"},"409":{"description":"Either a replayed nonce (pillar Mandate/Replay, reason_code replay_detected) or, when Idempotency-Key is sent, the original request with that key is still running (reason_code idempotency_request_in_progress; resend unchanged after `retry-after`). The second case is not recorded as a threat."},"422":{"description":"Idempotency-Key reused with a different request body within 24 hours (reason_code idempotency_key_reused). Nothing was executed."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/x402/decision":{"post":{"operationId":"x402_decision_post","summary":"Paid: decision + settlement","description":"Same shape as /api/decide, plus x402 settlement ($0.30/call). Unpaid requests get a 402 with a payment-required quote header.\n\nExample:\ncurl -i -X POST \"https://oceanalt.com/api/x402/decision\" -H \"content-type: application/json\" -d '{\"to\":\"0x…\"}'","tags":["paid (x402)"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"type":"string","description":"Payee address"},"amountUsdc":{"type":"number","description":"Amount"},"network":{"type":"string","description":"Chain"},"lang":{"type":"string","description":"en (default) or zh"}},"required":["to"]}}}},"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionResult"}}}},"400":{"description":"Missing or invalid address"},"402":{"description":"Payment required — the `payment-required` header carries the x402 v2 quote (accepts[])."},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/attestation/verify":{"get":{"operationId":"attestation_verify_get","summary":"Verify an attestation","description":"Anyone can verify an OceanAlt attestation: whether we signed it, which address it covers, what the decision was, and whether it has expired. No signup.\n\nExample:\ncurl \"https://oceanalt.com/api/attestation/verify?token=…\"","tags":["free"],"parameters":[{"name":"token","in":"query","required":true,"description":"Attestation token (returned by the decision endpoints)","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/mcp":{"post":{"operationId":"mcp_post","summary":"MCP over HTTP","description":"Model Context Protocol endpoint (JSON-RPC). The npm package oceanalt-aml-mcp offers the same tools over stdio.\n\nExample:\ncurl -X POST \"https://oceanalt.com/api/mcp\" -H \"content-type: application/json\"   -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'","tags":["free"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/incidents/stats":{"get":{"operationId":"incidents_stats_get","summary":"Incident archive stats","description":"Machine-readable summary of the public crypto incident archive (DefiLlama Hacks): totals, the classes where pre-payment screening partly applies with their share by count and by amount, and the per-class judgement with notes. Recomputed from the database (cached 10 minutes). \"Partly applies\" means screening covers part of a class, not that those incidents would all have been stopped. Returns 503 rather than zero when the archive cannot be read.\n\nExample:\ncurl \"https://oceanalt.com/api/incidents/stats\"","tags":["free"],"parameters":[{"name":"lang","in":"query","required":false,"description":"en (default) or zh","schema":{"type":"string"}}],"responses":{"200":{"description":"Screening / decision result","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing or invalid address"},"429":{"description":"Rate limited (60 requests/min per IP). Honour `retry-after`."}}}},"/api/access-keys/settings":{"get":{"operationId":"api_key_settings_get","summary":"Read one API key's restrictions","description":"Send the key itself (`Authorization: Bearer <key>` or `x-api-key`). Without a key this returns a self-description instead.\n\nOptional. Limit one key to any subset of: screen_address, screen_endpoint, decide, payee_decide, batch_screen, trace_funds, evidence_bundle, intent_check, baseline_lookup. A key with no scopes set — which includes every key issued before this existed — may call all of them, and any scope added later. A call outside a key's scopes is refused with HTTP 403 and reason_code key_scope_denied.\n\nOptional. Up to 20 IPv4 or IPv6 addresses or CIDR ranges. A call from any other address is refused with HTTP 403 and reason_code key_ip_denied. The source address is the one our reverse proxy records for the connection; an X-Forwarded-For header sent by the caller is not trusted. IPv4 and IPv6 are matched separately, so ::/0 does not cover IPv4 callers.\n\nOptional expires_at. Past it the key is treated exactly like a revoked key: the call is still served, at the anonymous level, and the response says key_rejected: expired. It is never silently downgraded.\n\nOnly the endpoints listed under these scopes, plus the MCP endpoint (see mcp), read an API key at all. What still ignores one, and why: the x402 paid endpoints (POST /api/x402/*), which are paid per call and never needed a key; the point-in-time lookup GET /api/risk?on=; and the remaining utility endpoints (/api/pay, /api/fulfillment, /api/baseline, /api/registry, the RAP endpoints). Those are rate limited per source IP, so scopes, allowlists and expiry do not apply there. Agent credentials (x-agent-id + x-agent-secret) are a separate mechanism and are unaffected by key restrictions.\n\nScopes govern what a key may do, not how much. On the six endpoints added on 2026-09-16 — batch, trace, flow, evidence bundles, intent checks, baseline lookups — a key that is sent is checked against its scopes, allowlist and expiry, but the call is still rate limited per source IP and still counts towards nobody's monthly allowance. Sending no key there behaves exactly as it did before: same rate limit, same response, no metering.","tags":["free"],"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}],"responses":{"200":{"description":"The key's current scopes, IP allowlist, expiry and any pending change."},"401":{"description":"No key, or a key we do not recognise."},"403":{"description":"The key is revoked, not activated, or has an IP allowlist and this request did not come from it (reason_code key_ip_denied)."}}},"post":{"operationId":"api_key_settings_post","summary":"Change one API key's restrictions","description":"Tightening — fewer scopes, a narrower allowlist, an earlier expiry — takes effect immediately when authenticated with the key itself, because tightening cannot grant anything. Loosening — more scopes, a wider or removed allowlist, a later or removed expiry — only takes effect after the holder opens a confirmation link emailed to the address the key was issued to (valid 24 hours) and confirms it there. A request that loosens anything waits as a whole. A key that has an IP allowlist can only be managed from an allowlisted address.","tags":["free"],"security":[{"ApiKeyBearer":[]},{"ApiKeyHeader":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","description":"Fields you leave out keep their current value.","properties":{"scopes":{"description":"Scopes this key may call, or \"all\".","oneOf":[{"type":"string","enum":["all"]},{"type":"array","items":{"type":"string","enum":["screen_address","screen_endpoint","decide","payee_decide","batch_screen","trace_funds","evidence_bundle","intent_check","baseline_lookup"]},"minItems":1}]},"ip_allowlist":{"type":"array","maxItems":20,"items":{"type":"string","description":"IPv4 or IPv6 address, or a CIDR range."},"description":"Empty means no IP restriction."},"expires_at":{"type":["string","null"],"format":"date-time","description":"null means no expiry."},"action":{"type":"string","enum":["confirm","cancel"],"description":"Used with `token` and `k` from a confirmation email instead of the fields above; no API key needed for these two."}}}}}},"responses":{"200":{"description":"Applied. Tightening takes effect immediately."},"202":{"description":"The change loosens something, so a confirmation email was sent to the address the key was issued to. Nothing changed yet."},"400":{"description":"Invalid scopes, IP entry or expiry; the `field` says which."},"401":{"description":"No key, or a key we do not recognise."},"403":{"description":"Revoked, not activated, or outside the key's own IP allowlist."},"410":{"description":"The confirmation link has expired (24 hours)."}}}}},"webhooks":{"key-alert":{"post":{"summary":"A payment by one of your agents was blocked or flagged","description":"Sent to the webhook URL configured in the console (https://oceanalt.com/en/keys).\n\nSignature (Standard Webhooks, https://www.standardwebhooks.com/): webhook-signature = \"v1,\" + base64(HMAC-SHA256(key, webhook-id + \".\" + webhook-timestamp + \".\" + raw body)), where key is the base64-decoded part of the whsec_ secret after the prefix. Reject a webhook-timestamp more than 300 seconds from your clock. For 24 hours after a rotation the header carries two space-separated signatures; accept if either matches.\n\nDelivery: answer 2xx within about 5 seconds. Redirects are not followed. Anything else is retried after 60, 300, 1800, 7200 seconds, up to 5 attempts in total, with the same webhook-id (deduplicate on it).","parameters":[{"name":"webhook-id","in":"header","required":true,"schema":{"type":"string"},"description":"Unique message id, identical across retries."},{"name":"webhook-timestamp","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds when this attempt was signed."},{"name":"webhook-signature","in":"header","required":true,"schema":{"type":"string"},"description":"Space-separated list of v1,<base64> signatures."},{"name":"X-OceanAlt-Event","in":"header","required":false,"schema":{"type":"string"},"description":"Event type, e.g. key-alert."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["key-alert"]},"agentId":{"type":"string"},"label":{"type":"string"},"kind":{"type":"string","description":"block | review"},"pillar":{"type":"string","description":"The gate that fired, e.g. Screening/AML."},"reason":{"type":"string"},"amountUsdc":{"type":["number","null"]},"toAddr":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}},"responses":{"2XX":{"description":"Acknowledged. Any other outcome counts as a failed delivery and is retried."}}}},"webhook.test":{"post":{"summary":"Test event sent from the console","description":"Signed exactly like key-alert. Test events are not retried.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["webhook.test"]},"note":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}}},"responses":{"2XX":{"description":"Acknowledged."}}}}},"components":{"schemas":{"ScreenResult":{"type":"object","properties":{"address":{"type":"string"},"verdict":{"type":"string","enum":["clear","caution","risky","uncertain"],"description":"uncertain means screening could not run — never treat it as clear."},"risk":{"type":["integer","null"],"minimum":0,"maximum":100},"blocked":{"type":"boolean"},"lang":{"type":"string","enum":["en","zh"],"description":"Language the human-readable fields were rendered in."},"signals":{"type":"array","items":{"type":"string"},"description":"Human-readable risk signals, in the requested language."},"signal_keys":{"type":"array","items":{"type":"string"},"description":"Language-independent signal keys (e.g. list.mixer, taint.onehop). Branch on these, not on the prose."},"evidence":{"type":"array","description":"Verifiable evidence: which list, label or on-chain path matched, with a link you can open yourself.","items":{"type":"object","properties":{"label":{"type":"string"},"detail":{"type":"string"},"source":{"type":"string"},"url":{"type":"string","format":"uri"}}}},"provider":{"type":"string"},"note":{"type":"string"},"reason_code":{"type":"string","description":"Stable machine-readable reason code (x402 conformance)."},"signal_class":{"type":"string","enum":["ok","payment","regulatory","authorization","uncertain"]},"retry":{"type":"object","description":"Whether and under what condition an agent should retry."},"advice":{"type":"string"},"standard":{"type":"string","format":"uri"}}},"DecisionResult":{"allOf":[{"$ref":"#/components/schemas/ScreenResult"},{"type":"object","properties":{"decision":{"type":"string","enum":["allow","review","decline"]},"allow":{"type":"boolean","description":"One-line branch for an agent: if (!allow) halt()."},"settlement":{"type":"object","description":"Compliance attestation you can bind to your own settlement. OceanAlt never touches the funds.","properties":{"attestation":{"type":"string"},"attestation_id":{"type":"string"},"verify_url":{"type":"string","format":"uri"},"expires_at":{"type":"integer"}}}}}]}},"securitySchemes":{"ApiKeyBearer":{"type":"http","scheme":"bearer","description":"Optional registered API key (POST /api/access-keys). It raises limits; it is never required."},"ApiKeyHeader":{"type":"apiKey","in":"header","name":"x-api-key","description":"The same key, for clients that prefer a custom header."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Honoured on the payment call (no action) and on action rotate; ignored by preview and other actions. Same x-agent-secret + same key + same JSON body within 24 hours → original status and body with Idempotent-Replayed: true, nothing executed again, no threat recorded. Same key + different body → 422. Original still running → 409 with retry-after. Keep the same nonce when retrying. A UUID is recommended."}}},"externalDocs":{"description":"RAP — Recursive Agent Payment standard","url":"https://oceanalt.com/en/rap"}}