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
Leave an email: 5,000/mo at full speed
Registered agent: 50,000/mo
Need more than that?
CORS enabled
Evidence, not just a score
Machine-readable spec
/openapi.json →Output language
/api/decideDecide 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.
curl "https://oceanalt.com/api/decide?to=0x8589427373D6D84E98730D7795D8f6f8731FDA16"{
"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", ... }
}/api/payee/decideThe 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.
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"}'{
"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."
}/api/riskSingle address
Screen one payee before paying: sanctions / mixer / our risk list, or high-risk on-chain signals? Returns a verdict plus verifiable evidence.
curl "https://oceanalt.com/api/risk?addr=0x8589427373D6D84E98730D7795D8f6f8731FDA16&network=ethereum"{
"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"
}/api/risk/batchBatch screening
Screen many addresses at once (deduped, up to 25). For OTC desks / batch jobs. Sorted risky → caution → clear.
curl -X POST "https://oceanalt.com/api/risk/batch" \
-H "content-type: application/json" \
-d '{"addresses":["0x8589...","TWd4...","T..."]}'{
"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" }
]
}/api/risk/flowFund-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.
curl "https://oceanalt.com/api/risk/flow?addr=TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb"{
"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.422idempotency_key_reused— The same key was used with a different body (for example, a new nonce on retry). Nothing ran.409idempotency_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.400idempotency_key_invalid— The key must be 1–255 printable ASCII characters; a UUID is recommended.
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.
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);
// });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.
screen_address— Address screening: one payee address (the ?on= point-in-time lookup does not read keys and is unaffected)screen_endpoint— Endpoint screening: is this payment URL a known phishing hostdecide— Pre-payment decision: allow / review / decline (including the optional intent check)payee_decide— Payee-side decision: accept this incoming payment or notbatch_screen— Batch screening: up to 25 addresses in one call (the paid POST /api/x402/batch authenticates by payment and does not read keys)trace_funds— Fund-flow tracing: multi-hop taint trace and counterparty flow analysis (two views of the same upstream data, kept in one scope)evidence_bundle— Evidence bundle: one address's Ed25519-signed evidence bundleintent_check— Intent check: what this calldata will actually do, against the payment you declaredbaseline_lookup— Control-baseline lookup: what controls a counterparty has attested to, before you settle
403key_scope_denied— The endpoint is outside this key's scopes. The response says which scope was refused and which ones the key allows.403key_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.200access.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.
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"}'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·arctron(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.
-
/api/x402/decision— full gateway verdict (allow/review/decline) + evidence · $0.30 -
/api/x402/trace?addr=T…— Tron multi-hop taint trace (≤3 hops) · $0.20 -
/api/x402/batch— batch screening (≤25) · $0.10
SDK & MCP (wire it into your agent)
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 });{
"mcpServers": {
"oceanalt-aml": {
"command": "npx", "args": ["-y", "oceanalt-aml-mcp"],
"env": { "OCEANALT_PAYER_KEY": "0x…" }
}
}
}
