# Audit a contract Source: https://serialized.mintlify.app/api-reference/audit-a-contract /api-reference/openapi.json get /audit-contract Returns a safety verdict, a human-readable summary, identity, and the detected risks for the given contract. Results are returned in milliseconds when the contract has been audited before; a first-time audit may take a few seconds. # Audit a Contract Source: https://serialized.mintlify.app/api-reference/audit-contract GET /audit-contract The single endpoint: send a contract, get a verdict. Audit any contract on a [supported chain](/supported-chains). Send a `GET` request with your API key in the `X-Auth-Key` header; the full parameter and response reference is below. By default the call is synchronous: it returns the cached verdict in milliseconds, or holds until a first-time audit completes. Add `async=true` to make first-time audits non-blocking: a cached verdict is still returned directly, but when an audit has to run you get `{ "status": "FETCHING" | "DECOMPILING" | "ANALYZING" }` immediately instead. Poll the same URL every 1-2 seconds until it flips to `{ "audit": ... }`, useful to drive a progress indicator. Billing is identical in both modes; progress responses are free. Or skip polling entirely: add `subscribe=true` to fire the audit, subscribe the token, and have the finished verdict (plus every later change) **pushed to you** over a live SSE stream or a webhook. See [SSE & Webhooks](/sse-and-webhooks). For how to read the result, see [Understanding Results](/understanding-results). For the full list of `type` values, see [Risk Categories](/risk-categories). # Bulk-subscribe from your 15-day call history Source: https://serialized.mintlify.app/api-reference/bulk-subscribe-from-your-15-day-call-history /api-reference/openapi.json post /subscriptions/import-history Subscribe every token you've called in the last 15 days whose audit is already at the current prompt version (old-version tokens are skipped so no re-audit wave is triggered). Two-step: a bare POST is a DRY-RUN quote (creates/charges nothing); re-POST with `?confirm=true` to subscribe and be charged. Cap-gated on confirm. # Get webhook config + 24h delivery health Source: https://serialized.mintlify.app/api-reference/get-webhook-config-+-24h-delivery-health /api-reference/openapi.json get /webhook Returns the current delivery URL (never the secret), whether a secret is set, a per-status delivery count over the last 24h, and the active subscription count. # List active subscriptions Source: https://serialized.mintlify.app/api-reference/list-active-subscriptions /api-reference/openapi.json get /subscriptions # Live push over Server-Sent Events (no inbound port) Source: https://serialized.mintlify.app/api-reference/live-push-over-server-sent-events-no-inbound-port /api-reference/openapi.json get /stream Hold this GET open to receive `audit.changed` events as they happen: a no-inbound-port way to receive pushes. No setup: it's a plain authenticated endpoint (your API key is all it needs). Each SSE frame is `event: audit.changed` with the SAME payload as a webhook and `id: `. Resume after a disconnect with the `Last-Event-ID` header (automatic in EventSource) or `?since=`; a fresh connect streams new events only (`?since=0` replays all). Scoped to your active subscriptions. A `:` keepalive comment is sent about every 25s. # Replay audit-change events (webhook catch-up) Source: https://serialized.mintlify.app/api-reference/replay-audit-change-events-webhook-catch-up /api-reference/openapi.json get /events The durable catch-up path: replay the `audit.changed` events for your currently-active subscriptions since a cursor. Page with `since=cursor` until `events` is empty. Scoped to active subscriptions; unsubscribing a token also hides its past events. Each event is projected to your response profile, exactly like the pushed webhook. # Send a signed test ping Source: https://serialized.mintlify.app/api-reference/send-a-signed-test-ping /api-reference/openapi.json post /webhook/test POSTs a signed `{ "type": "ping" }` to your configured URL so you can validate wiring + signature verification. The ping is NOT an `audit.changed` event and carries a non-UUID event id / `x-serialized-event: ping`. # Set the delivery endpoint (push webhook) Source: https://serialized.mintlify.app/api-reference/set-the-delivery-endpoint-push-webhook /api-reference/openapi.json put /webhook Register the HTTPS URL that audit-change events are POSTed to, and get an HMAC signing secret (returned ONCE). Updating the URL keeps the existing secret; pass `rotate: true` to force a new one. Subscribing does NOT require a webhook; it only controls push delivery. See SSE & Webhooks. # Subscribe one or many tokens Source: https://serialized.mintlify.app/api-reference/subscribe-one-or-many-tokens /api-reference/openapi.json post /subscriptions Subscribe a token (or up to 5000 in one call) to push/webhook updates. Idempotent: only genuinely-new tokens are charged (2 credits each). A bulk cost that would cross your cap is denied 402/429 (`action: raise_cap`) before anything is subscribed. # Unsubscribe all tokens Source: https://serialized.mintlify.app/api-reference/unsubscribe-all-tokens /api-reference/openapi.json delete /subscriptions # Unsubscribe one token Source: https://serialized.mintlify.app/api-reference/unsubscribe-one-token /api-reference/openapi.json delete /subscriptions/{chain}/{address} # Authentication Source: https://serialized.mintlify.app/authentication Authenticate every request with your secret API key. Every request is authenticated with a secret key sent in the `X-Auth-Key` header. ```bash theme={null} curl "https://www.serializedaudit.io/api/audit-contract?chain=base&address=0x..." \ -H "X-Auth-Key: sk_live_your_key_here" ``` ## Getting a key Generate keys from the **API keys** section of your [dashboard](https://www.serializedaudit.io/portal). You can create several, name them per environment, and revoke or rotate any of them independently. The secret is shown **once** at creation, so copy it then. ## Keep your key secret Your key authenticates billable requests. Anyone who has it can spend your credits. Call the API from your backend. Never ship a key in a browser bundle, mobile app, or public repo. If a key leaks, rotate it from the dashboard. The old secret stops working immediately. ## Errors A missing or invalid key returns `401 Unauthorized`. See [Errors & Status Codes](/errors). # Credits & Billing Source: https://serialized.mintlify.app/credits What a call costs and how billing works. Each plan includes a monthly allowance of **credits**. Every audit draws from it. Re-checking a contract you've already audited is the cheapest path. ## What a call costs | Call | Credits | | ----------------------------------------------------------- | ------- | | Re-check a contract you've already audited | 1 | | New audit, verified source | 4 | | New audit, closed source | 16 | | Subscribe a token to push updates (one-time, per new token) | 2 | A new audit costs **4 credits** for a contract with verified, published source. Unverified (closed-source) contracts cost **16 credits**, since they require more work to analyze. Subscribing a token to [push updates](/sse-and-webhooks) is a **one-time 2 credits** per token, charged only for genuinely new subscriptions (re-subscribing an existing token is free), and any re-audit the watcher later triggers is included at no extra cost. A bulk subscribe of *N* new tokens therefore costs *N* × 2. Credits are billed per account, not per key; all of your keys share one allowance. ## Plans and overage Choose a monthly plan that fits your volume on the [pricing page](https://www.serializedaudit.io/portal/plans). Credits reset each billing period. With pay-as-you-go enabled, calls beyond your allowance are billed automatically; otherwise the API returns `402` once the allowance is reached (see [Errors](/errors)). New accounts start with **free trial credits** so you can integrate before subscribing. ## Tracking usage Your [dashboard](https://www.serializedaudit.io/portal) shows credits used and remaining for the current period, usage over time, and recent calls. Enterprise plans offer custom volume and terms. Talk to us. # Errors & Status Codes Source: https://serialized.mintlify.app/errors What each status means and how to handle it. Errors return a JSON body with an `error` message: ```json theme={null} { "error": "Human-readable error message." } ``` ## Status codes | Status | Meaning | What to do | | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `200` | Success: the audit verdict is in the body (or, with `async=true`, a `{ "status": ... }` progress body). | Read `audit.isSafe`; if the body has `status`, keep polling. | | `400` | Invalid request: unknown `chain` or malformed `address`. | Fix the parameters; don't retry as-is. | | `401` | Missing or invalid API key. | Check the `X-Auth-Key` header and the key's status. | | `402` | No active subscription, or your credit allowance is exhausted. | See the `action` field (below) and top up or subscribe. | | `404` | The contract is not tradable yet (no liquidity pool). | Retry once a pool exists (see below). | | `429` | Spend cap reached. On `/audit-contract` the body carries `cap_usd` and `used_usd` (no `action`). On the subscribe endpoints (`POST /api/subscriptions`, `import-history`) a projected bulk cost that would cross the cap returns `action: "raise_cap"` with `required_credits` / `remaining_credits`, and nothing is subscribed. | Raise the cap in the dashboard / with your account manager; this is not a `5xx`, don't blind-retry. | | `5xx` | Temporary server error. | Retry with exponential backoff. | ## Not tradable yet (`404`) Only tradable contracts are audited. If no liquidity pool exists for the contract yet, the response is a `404` with a machine-readable reason rather than a generic error: ```json theme={null} { "audit": null, "reason": "no_liquidity_pool", "message": "No liquidity pool found for this token yet — only tradable contracts are audited. Retry once a pool has been created." } ``` Treat this as "not ready yet", not a hard failure. Retry once the contract has a pool. ## Handling `402` A `402` includes an `action` hint for what to do next: ```json theme={null} { "error": "Monthly credit limit reached", "action": "upgrade" } ``` | `action` | Meaning | | --------------------------------------- | --------------------------------------------------------------------------------------------- | | `subscribe` | No plan yet: choose one on the [pricing page](https://www.serializedaudit.io/portal/plans). | | `add_payment` | A payment failed: update your card in the dashboard. | | `resubscribe` | The subscription ended: start a new one. | | `upgrade` | Out of credits for the period: upgrade or enable pay-as-you-go. | | `wait` | A time-based limit is active (e.g. daily trial cap): retry after the `resets_at` in the body. | | `verify_payment` / `payment_processing` | Pay-as-you-go collection in progress: no action, retry shortly. | | `raise_cap` | Your own spending cap was hit: raise it in the dashboard. | Denial bodies may carry extra context fields depending on the wall (`resets_at`, `daily_limit`, `grace_usd`, `current_period_end`, `subscription_status`). Treat the shape as open. Treat `400` and `401` as permanent (fix the request), and `5xx` as transient (retry). For `402` and `429`, surface the response to whoever manages billing; neither clears on a blind retry. # How to integrate Source: https://serialized.mintlify.app/integration-guide The recommended integration: fire-and-subscribe, one SSE stream, no polling, no TTL. This is the integration we recommend to every new client. It is the pattern our highest-volume production integrations run on. Three rules cover it: 1. **Fire-and-subscribe.** The first time a token appears on your side, call [`/audit-contract`](/api-reference/audit-contract) with `async=true&subscribe=true`. The call returns immediately, and the finished audit is pushed to you. 2. **Hold one SSE stream open.** Keep a single long-lived `GET /api/stream` connection. Every finished audit and every later verdict change arrives there as an `audit.changed` event carrying the complete audit object. 3. **Your cache follows the stream.** Store what the stream sends, keyed by `(chain, address)`. No TTL, no refresh calls, no staleness logic. The stream is the source of truth. ## 1. Fire the audit ```bash theme={null} curl "https://www.serializedaudit.io/api/audit-contract?chain=base&address=0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8&async=true&subscribe=true" \ -H "X-Auth-Key: sk_live_your_key_here" ``` Two possible responses: * `{ "audit": { ... } }`: the token was already audited. Store it, you are done. The subscription is created either way. * `{ "status": "FETCHING" | "DECOMPILING" | "ANALYZING" }`: a fresh audit is running. Do nothing. The finished verdict arrives on your stream. ## 2. Hold the stream ```ts theme={null} import EventSource from "eventsource"; // any server-side SSE client works const es = new EventSource("https://www.serializedaudit.io/api/stream", { headers: { "X-Auth-Key": process.env.SERIALIZED_API_KEY }, }); es.addEventListener("audit.changed", (e) => { const evt = JSON.parse(e.data); cache.set(`${evt.chain}:${evt.address}`, evt.audit); }); ``` * The stream carries events for the tokens you are subscribed to. * `evt.audit` is the complete new verdict, the exact same object `/audit-contract` returns. Storing it is the entire update. * Reconnection and resume are automatic (`Last-Event-ID`): after a disconnect you receive everything you missed, in order. * We send a keepalive comment every 25 seconds. Reconnect on any disconnect, with your own backoff. ## That is the whole loop What it gives you: * Every subscribed token is re-checked onchain continuously, and every verdict change is pushed: a safe/unsafe flip, an owner change or renounce, a gate or a honeypot turning on or off. * Re-audits we trigger are free, including closed-source tokens whose source verifies later: you receive the verified verdict automatically. * Your users always see the current verdict. There is no stale-data window. * Your credit spend drops: a subscription is 2 credits once per token, instead of refresh calls forever. See [Credits & Billing](/credits). ## What NOT to build * **No polling loop.** Do not re-call `/audit-contract` to keep a verdict fresh. Changes come to you. * **No cache TTL.** A stored verdict stays valid until an `audit.changed` event replaces it. * **No re-fetch on push.** The event already contains the full audit. Do not call the API back. * **No status polling.** With `async=true&subscribe=true` the result is pushed. Polling still works, it is just never needed. ## Alternatives, when they fit better * **Synchronous + subscribe**: call `/audit-contract` with only `subscribe=true` and get the audit directly in the response (the call stays open while a fresh audit runs). The stream still handles all future changes. * **Webhooks instead of SSE**: we POST each event, HMAC-signed, to your HTTPS endpoint. Same payloads, same guarantees. See [SSE & Webhooks](/sse-and-webhooks). * **Catch-up replay**: `GET /api/events?since=` returns anything you missed. It is the durable backstop behind both transports. For LLMs and coding agents: the full documentation is one markdown file at [https://www.serializedaudit.io/llms-full.txt](https://www.serializedaudit.io/llms-full.txt). # Introduction Source: https://serialized.mintlify.app/introduction Instant smart-contract risk verdicts over a single REST endpoint. Serialized Audit tells you whether a smart contract is safe to trade. Send a contract address and chain, get back a clear verdict, a human-readable summary, and the specific risks we found. Contracts we've already seen return in milliseconds; a first-time audit takes a few seconds. It catches owner privileges and trap mechanisms that ordinary scanners miss, across 23 EVM chains, through one endpoint. ## How it works Sign up, then generate an API key from your [dashboard](https://www.serializedaudit.io/portal). New accounts include free trial credits, no card required. Send a `GET` request to `/audit-contract` with the contract `address` and `chain`, authenticated with your key. Branch on `isSafe`, show the `description` to your users, and inspect `vulnerabilities` for the detected risk categories. ## Why Serialized Audit A single request returns a trade-safety decision. No pipeline to assemble, no rules to maintain on your side. Detects intentional, obfuscated, and owner-controlled risks, not just known-bad signatures. One integration covers Ethereum, Base, BSC, Arbitrum, and 19 more. See [Supported Chains](/supported-chains). Cached verdicts return in milliseconds. Poll at high volume without paying full-audit latency. ## Base URL All requests go to: ``` https://www.serializedaudit.io/api ``` ## Quick example ```bash cURL theme={null} curl "https://www.serializedaudit.io/api/audit-contract?chain=base&address=0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8" \ -H "X-Auth-Key: sk_live_your_key_here" ``` ```ts TypeScript theme={null} const res = await fetch( "https://www.serializedaudit.io/api/audit-contract?chain=base&address=0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8", { headers: { "X-Auth-Key": process.env.SERIALIZED_API_KEY! } }, ); const { audit } = await res.json(); console.log(audit.isSafe, audit.description); ``` ```python Python theme={null} import os, requests res = requests.get( "https://www.serializedaudit.io/api/audit-contract", params={"chain": "base", "address": "0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8"}, headers={"X-Auth-Key": os.environ["SERIALIZED_API_KEY"]}, ) audit = res.json()["audit"] print(audit["isSafe"], audit["description"]) ``` Ready to integrate? Start with the [Quickstart](/quickstart), then see the [Audit a Contract](/api-reference/audit-contract) reference. # Re-audits & labeling Source: https://serialized.mintlify.app/programs Work with our team beyond the automated audit: a human deep-dive re-audit, or the launchpad labeling program. Every verdict on Serialized comes out of the automated pipeline. Source code in, onchain gates resolved, a verdict out, in seconds. Two paid options sit on top of it: a human re-audit of a single contract, and a labeling program for launchpads. A manual review of one deployed token by our security team. \$500, one time. Your name and badge on every token you deploy, plus VIP support and monthly re-audits. \$1,000 per month. ## At a glance | | Deep re-audit | Launchpad labeling | | -------------------- | -------------------------------- | -------------------------------------------------- | | Price | \$500, one time | \$1,000 per month | | Who it is for | Token teams | Launchpads | | What you get | A human review of one contract | Name, badge and backlink on every token you deploy | | Effect | Answer within 24 hours | Applies to your past and future tokens | | Changes the verdict? | Only if the current one is wrong | Never | | Payment | Stripe or crypto | Stripe or crypto | ## Deep contract re-audit The automated pipeline is fast and runs on every token. A deep re-audit is for when you want a person to look at a specific contract: you are about to launch, or you believe a verdict is wrong and want it checked. An engineer re-examines the full source, the current onchain state and the standing verdict, beyond what the automated pass does. Your request goes to our priority queue and we get back to you within 24 hours at the contact you provide. The fee covers the review, not the outcome. If the current verdict is correct, we explain why. If it is our mistake, we fix it. Tokens with identical bytecode share one audit. Review a single deployment from your factory and every future token with that same bytecode is covered, so you never pay per token. Request one on the [re-audit page](https://www.serializedaudit.io/reaudit). ## Launchpad labeling Anyone can spin up a launchpad. Labeling lets traders see which team is behind a token: a launchpad pays to show its name and badge on the audit page of every token deployed through it, with a backlink to its platform. The label is about provenance, not safety. It never marks a token as safe and never changes a verdict. A risky token from a labeled launchpad still shows red. The program includes: * Your name and badge on every token you deploy * A backlink to your platform, in front of everyone checking one of your tokens * VIP priority support on Telegram * Up to 2 deep contract re-audits per month Sign up on the [labeling page](https://www.serializedaudit.io/labeling). ### Recognized launchpads Some launchpads are already recognized on the tokens they deploy, without the program. That status is not for sale. We grant it at our own discretion, driven mainly by trading volume and user demand, and larger launchpads tend to earn it over time. If your launchpad is not recognized yet and you want your name shown now, labeling is the direct path to the same visibility, plus the backlink, VIP support and monthly re-audits above. ## Paying in crypto Both options can be paid in crypto instead of by card. Start on the relevant page, then reach out on [Telegram](https://t.me/ijaack94) and we will set it up manually. # Quickstart Source: https://serialized.mintlify.app/quickstart Run your first audit in under two minutes. ## 1. Get an API key Create an account, then open your [dashboard](https://www.serializedaudit.io/portal) and generate a key under **API keys**. The secret is shown once, so store it somewhere safe. New accounts include free trial credits. No card required. ## 2. Make your first call Pass your key in the `X-Auth-Key` header and the contract you want to check as query parameters. ```bash cURL theme={null} curl "https://www.serializedaudit.io/api/audit-contract?chain=base&address=0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8" \ -H "X-Auth-Key: sk_live_your_key_here" ``` ```ts TypeScript theme={null} const res = await fetch( "https://www.serializedaudit.io/api/audit-contract?chain=base&address=0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8", { headers: { "X-Auth-Key": process.env.SERIALIZED_API_KEY! } }, ); const { audit } = await res.json(); ``` ```python Python theme={null} import os, requests res = requests.get( "https://www.serializedaudit.io/api/audit-contract", params={"chain": "base", "address": "0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8"}, headers={"X-Auth-Key": os.environ["SERIALIZED_API_KEY"]}, ) ``` ## 3. Read the verdict A safe contract returns an empty `vulnerabilities` array: ```json theme={null} { "audit": { "isSafe": true, "description": "Standard ERC-20 token. No owner privileges that can harm holders were detected.", "symbol": "EXMPL", "address": "0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8", "chain": "BASE", "vulnerabilities": [] }, "billing": { "type": "fresh_no_decompile", "credits": 4 } } ``` Every authenticated call also echoes a `billing` object next to `audit`: the tier that was charged and the credits it cost. See [Credits & Billing](/credits). An unsafe one lists the risks it found: ```json theme={null} { "audit": { "isSafe": false, "description": "The owner can mint unlimited supply after deployment, diluting holders at will.", "symbol": "RISK", "vulnerabilities": [ { "type": "UnlimitedMinting", "impact": "critical", "description": "The owner can mint new tokens without limit." } ] } } ``` Branch on `isSafe` for your decision, surface `description` to users, and read `vulnerabilities` for details. See [Understanding Results](/understanding-results). ## Next steps The recommended production pattern: fire-and-subscribe plus one SSE stream. Keys, headers, and key safety. Full endpoint reference, every parameter and response. Every risk type we can return. What a call costs. # Risks Source: https://serialized.mintlify.app/risk-categories The shape of each detected risk, and the full set of categories. Each entry in `vulnerabilities` describes one detected risk. The fields below tell you what it is, how serious it is, and whether it is currently active. ## The risk object The risk category. See the [full list](#categories) below. Treat it as an open set and handle an unknown value gracefully. How seriously this risk affects a holder: `critical`, `warning`, or `info`. A plain-language explanation of the risk. A representative snippet of the relevant code for this risk. All relevant snippets when several functions share the same risk. `code` is the first of them. Present only when more than one applies. Whether this specific risk is currently neutralized, for example when the controlling owner has renounced. `null` when not applicable. A display-ready, human-readable explanation of the current state, e.g. `"Owner renounced"` or `"MINTER_ROLE has 2 active holders"`. ## Impact tiers | Impact | Meaning | | ---------- | -------------------------------------------------------------------------- | | `critical` | Can directly cause loss: funds drained, supply inflated, transfers seized. | | `warning` | Owner-controlled behavior that can harm holders under certain conditions. | | `info` | Worth surfacing, but low risk on its own. | ## Categories The `type` field uses the values below. The set is additive. New categories may be introduced, so handle an unknown value gracefully. | Category | What it means for a holder | | ---------------------- | --------------------------------------------------------------------------------------------------------- | | `UnlimitedMinting` | The supply can be inflated after launch, diluting holders. | | `UnauthorizedTransfer` | Tokens can be moved out of holder wallets without consent, or unlimited allowances extracted. | | `HiddenFees` | Transfer fees exist or can be changed at will, skimming value from trades. | | `LiquidityDrain` | Contract funds or pool liquidity can be withdrawn by a privileged party: a rug pull. | | `MaliciousUpgrade` | The contract can be upgraded or destroyed, changing its behavior after you've bought in. | | `UnsafeExternalCall` | Unsafe external calls or reentrancy that can be exploited to drain funds. | | `BlacklistAbuse` | Specific addresses can be frozen or blocked from transferring. | | `TradeRestriction` | Transfers can be blocked, throttled, or conditioned: trading toggles, cooldowns, or per-transaction caps. | | `Other` | A holder-impacting risk that doesn't fit the categories above. Used sparingly. | To show only actively dangerous risks, filter to `impact !== "info"` and `mitigated !== true`. Use `gateReason` for a ready-to-display status line. # SSE & Webhooks Source: https://serialized.mintlify.app/sse-and-webhooks PUT /webhook, GET /webhook, POST /webhook/test, GET /subscriptions, POST /subscriptions, DELETE /subscriptions/{chain}/{address}, DELETE /subscriptions, POST /subscriptions/import-history, GET /events, GET /stream Subscribe a token once, get pushed every verdict change. No polling. Instead of polling [`/audit-contract`](/api-reference/audit-contract) to keep a token's verdict fresh, **subscribe it once** and we push every change to you. Whenever a verdict actually moves (a safe↔unsafe flip, an owner change, a gate or honeypot flipping, or a closed-source token re-audited on its now-verified source), we deliver it over a live SSE stream or a webhook. Polling still works exactly as before; push delivery is purely additive. A subscription costs a one-time **2 credits** per token (only genuinely-new tokens are charged), and any re-audit the watcher triggers is **free** (included). See [Credits & Billing](/credits). **Two ways to receive the pushes (use either or both):** * **Live stream (SSE).** Hold an outbound `GET /api/stream` open and we push changes down it. No inbound port to expose: it's a normal long-lived HTTPS request with your API key. See [Live stream](#live-stream-sse) below. * **Webhook.** We `POST` each change to an HTTPS URL you host. Same events, same per-token subscriptions, same billing. The transport is your choice. Subscribing and delivery are independent. You can subscribe tokens **without** a stream or webhook configured. You're still charged, and you pull the changes yourself from [`GET /api/events`](#delivery-guarantees). A webhook or an open stream only controls whether changes are **pushed** to you; neither is a prerequisite for subscribing. ## Live stream (SSE) You open an outbound connection to us and we push events over it. **Nothing to expose, no inbound port.** It's a long-lived HTTPS `GET`, so a locked-down environment accepts it like any other API call. **Just connect.** Hold the request open and events arrive as they happen. Nothing to enable; it's a plain authenticated endpoint, and you only receive changes for tokens you're subscribed to: ```bash theme={null} curl -N https://www.serializedaudit.io/api/stream -H "X-Auth-Key: $KEY" ``` **A silent stream almost always means you have no subscriptions yet.** The connection opens and authenticates even with zero subscribed tokens, and then stays quiet forever, because there is nothing to push. If all you see is the opening `ready` event followed by `:` keepalives, check [`GET /api/subscriptions`](#subscribe-tokens) before debugging anything else. Subscribe as you go with `subscribe=true` on your audit calls, or in bulk with [`import-history`](#subscribe-tokens). You get [Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events): each change is one `event: audit.changed` frame whose `data:` is the same payload we deliver to a webhook (see [The event you receive](#the-event-you-receive)) and whose SSE `id:` is that event's sequential paging id (what you resume from, and **not** the `event_id` UUID inside the payload). ``` event: ready data: {"since": 766042} event: audit.changed id: 766083 data: { "type": "audit.changed", "chain": "ROBINHOOD", "audit": { ... }, "diff": { ... }, ... } ``` * **Resume with no gaps.** On reconnect, send the last id back. The browser `EventSource` does it automatically via `Last-Event-ID`; from code, pass the header or `?since=`. We replay every event after it, so a dropped connection never loses a change. A fresh connect (no id) streams **new** events only; `?since=0` replays your whole history. * **It's scoped to your active subscriptions**, exactly like a webhook and `/events`. * **Keepalive.** We send a `:` comment every \~25s so proxies keep the connection open. Reconnect on any disconnect (add your own backoff). * **Both at once is fine.** If you also have a webhook set, you'll receive each change on both, so dedupe on `event_id`. Browser example: ```js theme={null} const es = new EventSource('https://www.serializedaudit.io/api/stream', { headers: { 'X-Auth-Key': KEY }, // server-side EventSource libs support headers }) es.addEventListener('audit.changed', (e) => update(JSON.parse(e.data))) // reconnection + Last-Event-ID resume are automatic ``` ## Set up a webhook Register your HTTPS URL and get a signing secret (returned **once**, so store it): ```bash theme={null} curl -X PUT https://www.serializedaudit.io/api/webhook \ -H "X-Auth-Key: $KEY" -H 'Content-Type: application/json' \ -d '{"url":"https://your-backend.example.com/serialized-webhook"}' # -> { "ok": true, "url": "...", "secret": "whsec_..." } ``` Updating the URL keeps the secret; pass `{"rotate": true}` to force a new one. `POST /api/webhook/test` sends a signed `ping` so you can confirm wiring before subscribing anything. `GET /api/webhook` returns the current config (never the secret) plus 24h delivery health: ```json theme={null} { "webhook_url": "https://your-backend.example.com/serialized-webhook", "has_secret": true, "deliveries_24h": { "delivered": 128, "pending": 2, "dead": 1 }, "subscriptions": 1543 } ``` The `ping` sent by `/api/webhook/test` has `type: "ping"` (not `"audit.changed"`) and a non-UUID `event_id` / `x-serialized-event: ping`. Ignore it or `200` it; don't parse it as a real event. ## Subscribe tokens ```bash theme={null} # one, or bulk (up to 5000) curl -X POST https://www.serializedaudit.io/api/subscriptions \ -H "X-Auth-Key: $KEY" -H 'Content-Type: application/json' \ -d '{"tokens":[{"chain":"ROBINHOOD","address":"0x..."}]}' ``` A subscribe returns `{ "ok": true, "subscribed": , "charged": }`. Only `charged` tokens (genuinely new) cost credits. To backfill, `POST /api/subscriptions/import-history` subscribes every token you've called in the last 15 days that is already at the current audit version. It's a **two-step** flow so you never get a surprise bill: a bare call is a **dry-run quote** (creates/charges nothing): ```json theme={null} { "dry_run": true, "window_days": 15, "current_version_only": true, "would_subscribe": 1543, "credits_per_token": 2, "credits_required": 3086, "confirm_required": true, "hint": "POST again with ?confirm=true to subscribe and be charged." } ``` Re-POST with `?confirm=true` to actually subscribe and be charged (returns the same `{ ok, subscribed, charged }` shape as above). A bulk subscribe (`POST /api/subscriptions` or `import-history?confirm=true`) is cap-gated: if the projected cost would cross your allowance it is denied with `402`/`429` (`action: "raise_cap"`, plus `required_credits` / `remaining_credits`) **before** anything is subscribed. Nothing partial happens. Run the dry-run first to size the spend. **Fire an audit and get the result pushed, no polling:** call `/audit-contract` with `async=true&subscribe=true`. It starts the audit, subscribes the token, and pushes the finished verdict to you (stream or webhook) when it lands. See the [`subscribe`](/api-reference/audit-contract) parameter. ## The event you receive Every change (on the stream or your webhook) carries the same JSON (`type: "audit.changed"`): ```json theme={null} { "type": "audit.changed", "event_id": "a816aafb-...", "chain": "ROBINHOOD", "address": "0x...", "audit_id": 766083, "created_at": "2026-07-24T18:46:59.000Z", "change": { "is_safe": { "before": true, "after": false, "changed": true }, "owner_changed": false, "vulns": { "added": 1, "removed": 0, "changed": 0 } }, "audit": { "isSafe": false, "vulnerabilities": [ ], "name": "…", "sourceType": "verified", "…": "…" }, "diff": { "added": [ ], "removed": [ ], "changed": [ ] } } ``` * **`audit`** is the complete new verdict: **the exact same object you get back from [`/audit-contract`](/api-reference/audit-contract)** for this token. Do `store[address] = payload.audit` and you're done; no re-fetch. (It's `null` only if the change left no audit, e.g. a manual lock removed.) * **`change`** is a compact summary for cheap routing/alerting (badge flips, "a critical was added") without parsing the full verdict. * **`diff`** tells you *which* findings moved: `added` / `removed` are full vuln objects, `changed` is `{ before, after, fields }`. | Field | Type | Notes | | ------------------ | -------------- | -------------------------------------------------------------------------------------------------------- | | `type` | string | always `"audit.changed"` | | `event_id` | string (UUID) | identity of the event, stable across redeliveries. **Dedupe on this.** | | `chain`, `address` | string | the token this change is about | | `audit_id` | number \| null | our audit row id. `null` when the change left no audit row | | `created_at` | string | ISO 8601 UTC instant, e.g. `"2026-07-24T18:46:59.000Z"` | | `change` | object | compact summary: `is_safe {before, after, changed}`, `owner_changed`, `vulns {added, removed, changed}` | | `audit` | object \| null | the complete new verdict, identical to an `/audit-contract` response. `null` if the change left no audit | | `diff` | object | `added` / `removed` are full vuln objects, `changed` is `{before, after, fields}` | | `id` | number | **only on `/api/events` elements**, not in the stream payload: the sequential paging id | **`event_id` and the paging id are two different values.** `event_id` is a UUID that identifies the event itself and never changes, even if it is delivered twice — **dedupe on it**. The paging id is a separate sequential number: it is the SSE frame's `id:`, the `id` field on each [`/api/events`](#delivery-guarantees) element, and what you pass back as `since` — **page on it**. On a **webhook**, this JSON is the POST body, with headers `x-serialized-signature: t=,v1=` and `x-serialized-event: `. On the **stream**, it's the SSE frame's `data:`, and the frame's `id:` is the sequential paging id (what `Last-Event-ID` resumes from). ## Verify the webhook signature Webhook POSTs are HMAC-signed (the stream needs no verification; it's authenticated by your API key on the connection). `v1 = HMAC_SHA256(secret, ".")`. Verify against the **raw body bytes**, in constant time, and reject if `t` is more than 300s off from now: ```ts theme={null} import crypto from 'node:crypto' function verify(rawBody: string, header: string, secret: string): boolean { const m = /t=(\d+),v1=([0-9a-f]+)/.exec(header || '') if (!m) return false const [, t, v1] = m if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) return false const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex') const a = Buffer.from(expected), b = Buffer.from(v1) return a.length === b.length && crypto.timingSafeEqual(a, b) } ``` ## Delivery guarantees * **At-least-once.** We retry once (\~30s after the first failure) on any non-2xx/timeout, then dead-letter. A push can arrive more than once, so **dedupe on `event_id`**. * **Respond 2xx fast** (10s timeout, webhook). Verify → enqueue → `200`; do your work async. * **Nothing is lost.** Every change is written to a durable outbox. Replay anything you missed: `GET /api/events?since=&limit=1000` → `{ "events": [...], "cursor": }`. Page with `since=cursor`. This is your catch-up path after downtime (and what the stream's `Last-Event-ID` resume draws on). `/events` is scoped to your **currently-active** subscriptions: unsubscribing a token (`DELETE`) also hides its past events from replay, so drain what you need before unsubscribing. Each element is the same `audit.changed` object shown above, plus an `id` — the sequential paging id. `cursor` is the `id` of the last element (a number, echoing `since` on an empty page). ```json theme={null} { "events": [ { "id": 406045, "type": "audit.changed", "event_id": "8067cc0a-…", "…": "…" } ], "cursor": 406045 } ``` **`since` means the opposite here than on the stream.** On `/api/events`, omitting `since` is the same as `since=0` and replays your **whole** history from the beginning. On `/api/stream`, omitting it gives you **new events only**. In practice, always call `/api/events` with `since=`. * **We pace webhook delivery** (\~3 pushes/sec to one endpoint), so a big initial catch-up is spread out, never dumped at once. Steady-state is well under 1/sec. ## Endpoints | Method | Path | Purpose | | -------------- | ----------------------------------------------------------- | ------------------------------------------------------ | | `GET` | `/api/stream` | hold open for a live SSE feed of your subscriptions | | `PUT` | `/api/webhook` | set URL `{url, rotate?}` → secret (once) | | `GET` | `/api/webhook` | config + 24h delivery health | | `POST` | `/api/webhook/test` | send a signed `ping` | | `GET` / `POST` | `/api/subscriptions` | list / subscribe (`{chain,address}` or `{tokens:[…]}`) | | `POST` | `/api/subscriptions/import-history` | backfill from your 15-day history (`?confirm=true`) | | `DELETE` | `/api/subscriptions/:chain/:address` · `/api/subscriptions` | unsubscribe one / all | | `GET` | `/api/events?since=&limit=` | replay outbox events (catch-up) | All authenticate with your API key in `X-Auth-Key`. # Support Source: https://serialized.mintlify.app/support Talk to the team. We're quick to respond. Integration questions, custom chains, and enterprise: message us directly. Manage keys, track usage, and handle billing. ## Enterprise Need high volume, custom terms, or a dedicated integration? [Reach out on Telegram](https://t.me/ijaack94) and we'll set you up. # Supported Chains Source: https://serialized.mintlify.app/supported-chains The 23 EVM chains you can audit, and the chain symbol to pass. Pass the chain **symbol** as the `chain` query parameter (case-insensitive). For example, `chain=base`. | Chain | Symbol | | --------------- | ----------- | | Ethereum | `ETH` | | Base | `BASE` | | BNB Smart Chain | `BSC` | | Arbitrum | `ARB` | | Optimism | `OP` | | Polygon | `POLYGON` | | Avalanche | `AVAX` | | Blast | `BLAST` | | Linea | `LINEA` | | Scroll | `SCROLL` | | Mantle | `MANTLE` | | Polygon zkEVM | `ZKEVM` | | zkSync | `ZKSYNC` | | Abstract | `ABSTRACT` | | Sonic | `SONIC` | | Ape | `APE` | | Monad | `MONAD` | | Plasma | `PLASMA` | | MegaETH | `MEGAETH` | | Hyperliquid | `HYPE` | | Robinhood | `ROBINHOOD` | | Arc | `ARC` | | Stable | `STABLE` | Need a chain that isn't listed? [Contact us](https://t.me/gsm0x). We add EVM chains on request. # Understanding Results Source: https://serialized.mintlify.app/understanding-results Every field in the response and what it represents. A response wraps the result under an `audit` object. The fields documented below are the **complete public response**: exactly what an API call returns, and the only fields you should build against. The [endpoint reference](/api-reference/audit-contract) shows them with example payloads; this page explains what each one represents. ## Verdict These fields carry the safety decision. The overall verdict, your decision boundary. `true` means no holder-harming risk was active at audit time. It combines the token verdict and, when present, the pool-hook verdict. The verdict for the token contract on its own, ignoring any associated pool hook. The verdict for the associated Uniswap v4 pool hook, or `null` when the token has no hook. A short, human-readable explanation of the verdict, safe to display to your users. The detected risks. Empty when the contract is safe. Each item describes one risk; see [Risk Categories](/risk-categories) for the full object. ## Identity | Field | Type | Represents | | --------- | -------------- | ------------------------------------------- | | `name` | string \| null | Token name. | | `symbol` | string \| null | Token symbol. | | `address` | string | The audited contract address (echoed back). | | `chain` | string | The chain symbol (echoed back). | ## Source | Field | Type | Represents | | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sourceType` | string | How the analyzed code was obtained: `verified` (published source), `decompiled` (reconstructed from on-chain bytecode), `b20` (a Base native B20 token with no bytecode to read; classified on-chain), or `none`. | | `b20Flags` | object \| null | Live on-chain flag snapshot for a `b20` token (transfer policies, paused features, rebase multiplier, supply cap). `null` on every other `sourceType`. | ## Proxy | Field | Type | Represents | | ----------------------- | -------------- | ---------------------------------------------------------------------------------------------- | | `isProxy` | boolean | Whether the contract is a proxy. When `true`, the verdict already reflects its implementation. | | `implementationAddress` | string \| null | For a proxy, the implementation that was audited. | ## Pool hook (Uniswap v4) | Field | Type | Represents | | ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------- | | `hookAddress` | string \| null | Address of the associated pool hook, when present. | | `hookAudit` | object \| null | The hook's own audit: `isSafe`, `description`, `vulnerabilities`, `address`, `isDecompiled`. `null` when there is no hook. | ## Timing & versioning | Field | Type | Represents | | -------------------------- | ------ | ---------------------------------------------------------------------------------------------------- | | `createdAt` | string | ISO timestamp of when this audit was produced. | | `auditSystemVersion` | string | The version that produced this result; gate new fields on a minimum version. | | `latestAuditSystemVersion` | string | The latest version. If it differs from `auditSystemVersion`, the result refreshes on the next visit. | ## Verdicts stay current The verdict reflects the contract's state at the time of your request. A contract flagged unsafe can later become safe, for example once its owner renounces. Re-checking the same contract picks up that change: results re-audit on their own as the contract evolves, and a stale result (`auditSystemVersion` below `latestAuditSystemVersion`) refreshes on the next request. ## Not tradable yet Only tradable contracts are audited. If no liquidity pool exists for the contract yet, the API returns `404` with `{ "audit": null, "reason": "no_liquidity_pool" }`. Retry once a pool has been created. See [Errors & Status Codes](/errors).