OceanAltOceanAlt

DEVELOPERS · FREE API

A pre-payment risk check for your agent or system

The same address-risk logic we use ourselves, as a free API: no signup, CORS-enabled, returns verifiable evidence. Drop it into your system or an AI agent's pre-settlement flow. Ask 'is this counterparty safe?' before paying.

No signup · free

Call directly, no API key (60/min and 2,000 calls a month, metered per source IP).

Leave an email: 5,000/mo at full speed

Self-serve a key, send Authorization: Bearer: 180/min. Past the allowance it only slows to 60/min. It is never cut off.

Registered agent: 50,000/mo

Already have an agent in the registry? Use its credential (x-agent-id + x-agent-secret): 300/min, limits aggregated per owner. No second key to apply for.

Need more than that?

Those three tiers are all of it, and all free: there is no paid plan. If you need more, tell us at /pricing what you're building and roughly how much you'd call it.

CORS enabled

Call from browser, agent, or server.

Evidence, not just a score

Which list, which on-chain path: all verifiable.

Want the higher limits? Self-serve a key with your email → The key appears once, in the email. We store only its hash.

Machine-readable spec

OpenAPI 3.1: so an agent can discover the parameters and response shapes instead of guessing from prose.

/openapi.json →

Output language

Human-readable fields default to English; add lang=zh for Chinese. Every response also carries signal_keys: language-independent keys agents should branch on instead of parsing prose.

GET / POST/api/decide

Decide before paying

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.

Parameters

  • to required — Payee address you're about to pay: 0x (EVM) / T (TRON)
  • amountUsdc optional — Amount in USDC (optional, recorded only)
  • purpose optional — Purpose/memo (optional)
  • network optional — EVM chain for 0x addresses (same as /api/risk)
  • task optional — 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)
  • reasoning optional — Payment intent: why this payee (optional)
  • maxAmountUsdc optional — Your declared per-payment cap in USDC (optional); amountUsdc above it → decline

Example

curl "https://oceanalt.com/api/decide?to=0x8589427373D6D84E98730D7795D8f6f8731FDA16"

Response

{
  "decision": "decline",         // allow | review | decline
  "allow": false,                // one-line gate: if (!allow) halt()
  "address": "0x8589...FDA16",
  "verdict": "risky",
  "risk": 100,
  "evidence": [ { "label": "hit mixer list", "detail": "Tornado Cash", "url": "..." } ],
  "retry_hint": "Do not retry; use a different payee.",
  "disclaimer": "OceanAlt has denied this service request. ...",   // on decline
  "intelligence": { "sources": [ /* OceanAlt AML + external adapter status */ ] },
  "tier": "free",
  "upgrade": "POST /api/x402/decision — same decision, plus settles on x402."
  // on allow/review: also "settlement": { "attestation_id", "verify_url", ... }
}
GET / POST/api/payee/decide

The payee decides too

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.

Parameters

  • payee required — Payee sid from the registry (see /registry.json)
  • payer.address optional — Payer address (screened in reverse)
  • payer.agentId optional — Payer agent's KYA id (required when the payee requires KYA)
  • amountUsdc optional — Amount (required when the payee sets limits)
  • purpose optional — Purpose (required when the payee whitelists purposes)

Example

curl -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"}'

Response

{
  "status": "decline",           // accept | decline | hold_request_info | not_ready
  "final": true,
  "reasons": [ "PAYER_SANCTIONED" ],
  "requested_fields": [],        // only on hold_request_info
  "decision_id": "pd_…",
  "expires_at": null,            // 48h on hold_request_info
  "payee_ref": { "sid": "…", "rating_status": "verified", "open_disputes": 0, "policy": { "min_usdc": null, "max_usdc": null, "require_kya": false, "purposes": [] } },
  "evidence": [ { "kind": "registry", "ref": "…/registry.json#sid", "complete_through": "…" }, { "kind": "screening", "ref": "…/api/risk?addr=…", "complete_through": "…" } ],
  "signature": { "token": "…", "verify_url": "…/api/attestation/verify?token=…" },
  "next": "Do not settle. Re-paying does not change this result."
}
GET/api/risk

Single address

Screen one payee before paying: sanctions / mixer / our risk list, or high-risk on-chain signals? Returns a verdict plus verifiable evidence.

Parameters

  • addr required — Address to screen: 0x (EVM) / T (TRON) / Solana base58
  • network optional — EVM chain for 0x addresses: ethereum (default) / base / bsc / polygon / arbitrum / optimism / avalanche

Example

curl "https://oceanalt.com/api/risk?addr=0x8589427373D6D84E98730D7795D8f6f8731FDA16&network=ethereum"

Response

{
  "address": "0x8589...FDA16",
  "verdict": "risky",           // risky | caution | clear
  "risk": 100,                   // 0-100
  "blocked": true,              // does the gateway block it?
  "signals": ["mixer contract"],
  "evidence": [                  // click-through, verifiable
    { "label": "hit mixer list", "detail": "Tornado Cash",
      "url": "https://etherscan.io/address/0x8589..." }
  ],
  "advice": "Decline. ...",
  "standard": "https://oceanalt.com/en/rap"
}
POST/api/risk/batch

Batch screening

Screen many addresses at once (deduped, up to 25). For OTC desks / batch jobs. Sorted risky → caution → clear.

Parameters

  • addresses required — Array of addresses (JSON body), up to 25

Example

curl -X POST "https://oceanalt.com/api/risk/batch" \
  -H "content-type: application/json" \
  -d '{"addresses":["0x8589...","TWd4...","T..."]}'

Response

{
  "summary": { "total": 3, "risky": 1, "caution": 0, "invalid": 1 },
  "results": [
    { "address": "0x8589...", "verdict": "risky", "risk": 100,
      "blocked": true,  "top": "mixer contract" },
    { "address": "TWd4...",    "verdict": "clear", "risk": 3,
      "blocked": false, "top": "has prior activity" }
  ]
}
GET/api/risk/flow

Fund-flow (Tron + EVM)

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.

Parameters

  • addr required — Tron address (T…) or 0x EVM address
  • network optional — EVM chain for 0x (ethereum default / base / polygon / arbitrum / optimism); omit for Tron

Example

curl "https://oceanalt.com/api/risk/flow?addr=TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb"

Response

{
  "chain": "tron",
  "total": 10000,
  "inbound":  [ { "address": "T...", "tag": "Binance-Hot",
                  "count": 17, "usdt": 2890000, "risky": false } ],
  "outbound": [ { "address": "T...", "tag": "OKX Hot",
                  "count": 25, "usdt": 3370000, "risky": false } ],
  "firstInflow": { "from": "T...", "usdt": 500, "date": "2024-06-01" },
  "riskyParties": 0
}

Idempotency-Key: retry payments safely

When the network drops, your agent cannot tell whether a payment request ran. Send an Idempotency-Key header with the POST /api/pay payment call (and with rotate), and keep the same key, the same nonce and the same body when you retry: within 24 hours we return the original response, nothing runs twice, and the retry is not recorded as a replay attack. Semantics follow the IETF draft draft-ietf-httpapi-idempotency-key-header.

curl -X POST "https://oceanalt.com/api/pay" \
  -H "content-type: application/json" \
  -H "x-agent-secret: $OCEANALT_AGENT_SECRET" \
  -H "Idempotency-Key: 8e03978e-40d5-43e8-bc93-6894a57f9324" \
  -d '{"agentId":"my-agent","amountUsdc":1,"to":"0x…","purpose":"api-credits","nonce":"b3f1c2…"}'
  • Idempotent-Replayed: true — A retry with the same key, body and credential: the original status code and body, unchanged.
  • 422 idempotency_key_reused — The same key was used with a different body (for example, a new nonce on retry). Nothing ran.
  • 409 idempotency_request_in_progress — The original request is still running. This is concurrency, not a replay attack, and is not recorded as a threat. Resend unchanged after retry-after seconds.
  • 400 idempotency_key_invalid — The key must be 1–255 printable ASCII characters; a UUID is recommended.

A replay must present the same x-agent-secret as the original, so someone who only knows the key cannot read your response. Calls without the header behave exactly as before. For a new payment, use a new key and a new nonce.

Webhook signatures

If you configure a webhook in the console (/keys), we POST a JSON body to it when one of your agent's payments is blocked. Every message is signed per the Standard Webhooks specification, so your system can confirm it came from OceanAlt, was not altered, and is not an old message replayed.

  • webhook-id — Unique message id; unchanged across retries of the same message, so deduplicate on it.
  • webhook-timestamp — Send time in Unix seconds. Reject anything more than 5 minutes from your current time.
  • webhook-signature — v1,<base64>: HMAC-SHA256 over webhook-id.webhook-timestamp.raw-body, keyed with the base64-decoded bytes after whsec_. For 24 hours after a rotation there are two signatures separated by a space; accept if either matches.

Verify in Node (no dependencies; this exact snippet runs in our own test suite):

import { createHmac, timingSafeEqual } from "node:crypto";

// secret:  the whsec_… value shown once in the console
// rawBody: the request body exactly as received (do NOT JSON.parse and re-stringify it)
export function verifyOceanAltWebhook(secret, headers, rawBody, toleranceSec = 300) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  const sigs = headers["webhook-signature"];
  if (!id || !ts || !sigs) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false; // stale or replayed

  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = createHmac("sha256", key).update(id + "." + ts + "." + rawBody).digest();

  // during a rotation there are two space-separated signatures; accept if either matches
  return sigs.split(" ").some((entry) => {
    const [version, b64] = entry.split(",");
    const got = Buffer.from(b64 || "", "base64");
    return version === "v1" && got.length === expected.length && timingSafeEqual(got, expected);
  });
}

// Express example:
// app.post("/hooks/oceanalt", express.raw({ type: "application/json" }), (req, res) => {
//   const raw = req.body.toString("utf8");
//   if (!verifyOceanAltWebhook(process.env.OCEANALT_WEBHOOK_SECRET, req.headers, raw)) return res.sendStatus(401);
//   const event = JSON.parse(raw); // then deduplicate on req.headers["webhook-id"]
//   res.sendStatus(204);
// });

Or use any Standard Webhooks library (standardwebhooks.com), passing it the whsec_ secret from the console.

The signing secret is generated in the console, shown once, and can be rotated at any time. Your endpoint should answer 2xx within about 5 seconds; failed deliveries are retried after 1 min, 5 min, 30 min and 2 h, up to 5 attempts in total, and redirects are not followed. Each attempt's status code, duration and error summary appear under Recent deliveries in the console for 30 days.

Locking a key down: scopes, IP allowlist, expiry

A key has no limits by default, and keys already in use are unaffected. Its holder can add limits, and those limits decide how much can be done with the key if it ever leaks.

Scopes: which endpoints the key may call

  • screen_address — Address screening: one payee address (the ?on= point-in-time lookup does not read keys and is unaffected)GET /api/risk
  • screen_endpoint — Endpoint screening: is this payment URL a known phishing hostGET /api/endpoint
  • decide — Pre-payment decision: allow / review / decline (including the optional intent check)GET /api/decide · POST /api/decide
  • payee_decide — Payee-side decision: accept this incoming payment or notPOST /api/payee/decide
  • batch_screen — Batch screening: up to 25 addresses in one call (the paid POST /api/x402/batch authenticates by payment and does not read keys)POST /api/risk/batch
  • trace_funds — Fund-flow tracing: multi-hop taint trace and counterparty flow analysis (two views of the same upstream data, kept in one scope)GET /api/risk/trace · GET /api/risk/flow
  • evidence_bundle — Evidence bundle: one address's Ed25519-signed evidence bundleGET /api/evidence/bundle
  • intent_check — Intent check: what this calldata will actually do, against the payment you declaredPOST /api/intent/check
  • baseline_lookup — Control-baseline lookup: what controls a counterparty has attested to, before you settleGET /api/baseline/lookup

Everything not listed above does not read a key and is rate limited per source IP: the x402 paid endpoints (paid per call, so they never needed a key), the point-in-time lookup GET /api/risk?on=, and the gateway, fulfillment and registry endpoints. The MCP endpoint POST /api/mcp takes a key optionally: without one every tool works as before; with one, tool calls are rate limited per key, the holder's scopes and IP allowlist apply per tool, and verdicts can go to the decision log (this is our own API key, not the OAuth authorization in the MCP specification). These locks also govern what a key may do, not how much: the six endpoints added on 2026-09-16 are still rate limited per source IP and still count towards nobody's monthly allowance: calling them without a key behaves exactly as it did before. Agent credentials (x-agent-id + x-agent-secret) are a separate mechanism and are unaffected. The decision log is off by default and records nothing until the holder switches it on; export it with GET /api/access-keys/decisions (JSON or CSV, signed).

  • 403 key_scope_denied — The endpoint is outside this key's scopes. The response says which scope was refused and which ones the key allows.
  • 403 key_ip_denied — The call did not come from the key's IP allowlist, up to 20 entries, each an address or a CIDR range (e.g. 198.51.100.0/24), with IPv4 and IPv6 matched separately. We use where the connection actually came from; an X-Forwarded-For header the caller sets itself does not count.
  • 200 access.key_rejected = expired — Past the expiry its holder set. Treated exactly like a revoked key: the call is still served, at the anonymous level, and the response says the key was not accepted, never a silent downgrade that sends you hunting through your own code.

Read and change the settings (authenticated with the key itself)

curl "https://oceanalt.com/api/access-keys/settings" -H "authorization: Bearer $OCEANALT_API_KEY"

curl -X POST "https://oceanalt.com/api/access-keys/settings" \
  -H "authorization: Bearer $OCEANALT_API_KEY" \
  -H "content-type: application/json" \
  -d '{"scopes":["screen_address","decide"],"ip_allowlist":["198.51.100.0/24"],"expires_at":"2027-01-01T00:00:00Z"}'

Tightening applies immediately with the key alone: fewer scopes, a narrower allowlist, an earlier expiry. Loosening needs a confirmation link emailed to the address the key was issued to: more scopes, a wider or removed allowlist, a later or removed expiry. Otherwise anyone who stole the key would simply remove its limits. If any part of a request loosens something, the whole change waits. Fields you leave out keep their current value.

Set it up in the browser →

What the verdict means

  • risky — Sanctions / mixer / our risk list, or high-risk on-chain → decline.
  • caution — Risk signals present → proceed only within your mandate.
  • clear — Screening completed, no risk signal found. Note: no signal ≠ proof of safety.
  • uncertain — Screening did not complete (upstream data unavailable right now). No risk score. Retry later, never treat it as clear.

Supported networks

8 EVM chains + Tron + Solana. Sanctions / mixer / scam checks are chain-agnostic; on-chain signals and issuer (USDT/USDC) freeze are queried live per chain, each verifiable.

  • ethereum · base · bsc · polygon · arbitrum · optimism · avalanche · arc
  • tron (USDT-TRC20 freeze / deep trace / fund-flow) · solana (sanctions list)

x402 paid tier (pay-per-call · no signup, no key)

Heavier analysis via x402: HTTP 402 → sign one USDC authorization → 200. The facilitator verifies, pays gas, and never custodies your funds. One line with the oceanalt-aml SDK, or any x402 client.

  • POST /api/x402/decision — full gateway verdict (allow/review/decline) + evidence · $0.30
  • GET /api/x402/trace?addr=T… — Tron multi-hop taint trace (≤3 hops) · $0.20
  • POST /api/x402/batch — batch screening (≤25) · $0.10

Machine-readable manifest (free vs paid / network / facilitator / pricing): /api/x402

What x402 is + try compliance pre-check in the browser: /en/x402

SDK & MCP (wire it into your agent)

① Programmatic SDK (Node / browser / Deno / Bun, zero-dependency free path):

npm i oceanalt-aml

import { OceanAltAML } from "oceanalt-aml";
const aml = new OceanAltAML();
const r = await aml.screen("0x…", { network: "optimism" });        // free
const d = await aml.decision({ to: "0x…", amountUsdc: 25 },        // paid (x402)
  { privateKey: process.env.PAYER_KEY });

② MCP server (native calls from Claude / any MCP client):

{
  "mcpServers": {
    "oceanalt-aml": {
      "command": "npx", "args": ["-y", "oceanalt-aml-mcp"],
      "env": { "OCEANALT_PAYER_KEY": "0x…" }
    }
  }
}

Free tools: screen_address, recent_flagged; paid: compliance_decision, deep_trace, batch_screen (needs OCEANALT_PAYER_KEY).

Fair use: free for personal and integration testing. For heavy or commercial use, get in touch. Signals only, not financial or trading advice.

x402-ecosystem callable (free): our AML is declared as an x402 resource at price=0, so agents call directly, no payment and no key. Machine-readable manifest: /api/x402.

→ Try address screening in the browser · Full compliance gateway (11 gates) · RAP standard