curl --request GET \
--url https://www.serializedaudit.io/api/audit-contract \
--header 'X-Auth-Key: <api-key>'import requests
url = "https://www.serializedaudit.io/api/audit-contract"
headers = {"X-Auth-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-Auth-Key': '<api-key>'}};
fetch('https://www.serializedaudit.io/api/audit-contract', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://www.serializedaudit.io/api/audit-contract",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-Auth-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://www.serializedaudit.io/api/audit-contract"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-Auth-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://www.serializedaudit.io/api/audit-contract")
.header("X-Auth-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.serializedaudit.io/api/audit-contract")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-Auth-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"audit": {
"isSafe": true,
"isTokenSafe": true,
"isHookSafe": null,
"description": "Standard ERC-20 token. No owner privileges that can harm holders were detected.",
"vulnerabilities": [],
"name": "Example Token",
"symbol": "EXMPL",
"address": "0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8",
"chain": "BASE",
"sourceType": "verified",
"isProxy": false,
"implementationAddress": null,
"hookAddress": null,
"hookAudit": null,
"createdAt": "2026-06-22T10:00:00.000Z",
"auditSystemVersion": "prod-v2.10",
"latestAuditSystemVersion": "prod-v2.10"
}
}Audit a Contract
The single endpoint: send a contract, get a verdict.
curl --request GET \
--url https://www.serializedaudit.io/api/audit-contract \
--header 'X-Auth-Key: <api-key>'import requests
url = "https://www.serializedaudit.io/api/audit-contract"
headers = {"X-Auth-Key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-Auth-Key': '<api-key>'}};
fetch('https://www.serializedaudit.io/api/audit-contract', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://www.serializedaudit.io/api/audit-contract",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-Auth-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://www.serializedaudit.io/api/audit-contract"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-Auth-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://www.serializedaudit.io/api/audit-contract")
.header("X-Auth-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://www.serializedaudit.io/api/audit-contract")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-Auth-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"audit": {
"isSafe": true,
"isTokenSafe": true,
"isHookSafe": null,
"description": "Standard ERC-20 token. No owner privileges that can harm holders were detected.",
"vulnerabilities": [],
"name": "Example Token",
"symbol": "EXMPL",
"address": "0x6D7401F6f1fB09ff24a048337ff44D890CdF86F8",
"chain": "BASE",
"sourceType": "verified",
"isProxy": false,
"implementationAddress": null,
"hookAddress": null,
"hookAudit": null,
"createdAt": "2026-06-22T10:00:00.000Z",
"auditSystemVersion": "prod-v2.10",
"latestAuditSystemVersion": "prod-v2.10"
}
}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.
address also accepts a liquidity pool (a Uniswap V2/V3 pair address or a Uniswap V4 pool id) or a V4 hook address. audit.entry tells you how it was addressed (token / pool / hook).
- Pool. Audits the token plus that specific pool’s hook, instead of the token’s primary pool. The response is the token’s audit:
audit.addressis the resolved token, andaudit.pooldescribes the pool the hook verdict was computed against (including whether it is the token’s primary pool). Store results keyed byaudit.chain+audit.address, not by the value you sent. - Hook. Returns the hook’s own audit, not a token’s (one hook often serves many tokens):
audit.addressis the hook,isSafe,descriptionandvulnerabilitiesare the hook’s verdict, andaudit.hookContextlists the pools and tokens that use it (pools,poolCount). For a token’s combined verdict, call with the token address and readisHookSafe/hookAudit.
404 { "reason": "pool_not_found" | "hook_not_found" }.
For how to read the result, see Understanding Results. For the full list of type values, see Risk Categories.
Solana tokens
Passchain=solana (or sol) and the token’s mint address as address. A Solana address is base58 and case-sensitive: send it exactly as written, never lowercased (a lowercased mint is a different account). The response echoes it unchanged in audit.address, so key your cache on it as-is.
curl "https://www.serializedaudit.io/api/audit-contract?chain=solana&address=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" \
-H "X-Auth-Key: sk_live_your_key_here"
sourceType: "solana" and the snapshot it was read from in solanaState.
What is checked
We judge the capability, not what the holder did with it so far: a live power to block a sale, seize balances, or raise a fee without a ceiling is critical whoever holds it (a wallet, a multisig or a program). A live mint authority is a warning: it can dilute holders but cannot block or seize their tokens, so on its own it leaves the token SAFE. The description names the holder in plain words. A power is cleared only when it is revoked or held by a burn address, and then it raises no finding at all.| Control | When it is a finding | type | impact |
|---|---|---|---|
| Freeze authority | Live: it can freeze any holder’s account, a pool’s vault included | BlacklistAbuse | critical |
| Default account state | New accounts start frozen | TradeRestriction | critical |
| Permanent delegate | Live: it can move or burn anyone’s tokens | UnauthorizedTransfer | critical |
| Transfer hook | A hook program is set (every transfer calls it), or none is set but its authority can attach one | UnsafeExternalCall | critical |
| Pause | Transfers paused now, or a live pause authority | TradeRestriction | critical |
| Non-transferable | The token can never move | TradeRestriction | critical |
| Transfer fee | Takes 100 % of a transfer | TradeRestriction | critical |
| Transfer fee | A live fee authority (it can raise the fee to 100 %), whatever today’s rate | HiddenFees | critical |
| Transfer fee | Fixed (authority revoked) above 10 % | HiddenFees | critical |
| Transfer fee | Fixed, 10 % or less | HiddenFees | warning |
| Mint authority | Live: it can create any amount | UnlimitedMinting | warning |
| Display scaling | A live authority can rescale displayed balances | Other | warning |
| Display interest | A live authority sets the displayed interest rate | Other | info |
| Metadata | Name, symbol and image can still be changed | Other | info |
| Unrecognized extension | A Token-2022 extension this audit does not judge yet | Other | critical |
solanaFlag (the control, e.g. freeze_authority) and solanaHolder (the address holding it); code is empty.
When a live power is held by the known issuer of that token (a regulated stablecoin issuer on its own stablecoin, for example) or by a program whose code we reviewed and which bounds the power, the finding stays listed with mitigated: true and a gateReason naming the reviewed party, and it no longer counts against isSafe. That never applies to a state actively used against holders: a pause that is on, accounts born frozen, a prohibitive fee.
What is not checked
- No code review and no LLM. There is no code to read.
- No market analysis. Holders, liquidity, volume, insiders and deployer history are not part of the verdict.
- The onchain sell simulation and the LP-lock warning are not available on Solana yet. When they are, they will appear exactly as on EVM chains.
- Pool and hook entries are EVM only. On Solana,
addressmust be the mint.
The solanaState object
solanaState is the snapshot the verdict was computed from (snake_case, like b20Flags), null on every other chain. Every power is an authority object { address, kind } where kind is revoked, burned, multisig (with m of n signers, and keys when fewer distinct keys than m are enough), wallet, pda (a program-controlled address, with its owner program) or program (the address of a deployed program, whose deploy key may still sign). The main fields: program (spl-token or spl-token-2022), mint_authority, freeze_authority, permanent_delegate, transfer_fee, transfer_hook, pausable, default_account_state, non_transferable, scaled_ui_amount, metadata, and unix_ts / read_slot (when it was read). The full schema is below.
Not a token mint
An address that is not a fungible token mint (a wallet, a pool, a token account, a program, an NFT) returns404 { "audit": null, "reason": "not_a_token", "message": "..." }, the same shape as the no-pool 404: branch on reason. Do not retry it: send the token’s mint address instead.
Push updates
Subscriptions work the same on Solana: subscribe withchain: "SOLANA" and the mint. Because the configuration is re-read on every refresh, a change such as an authority revoked, a fee raised or a pause switched on is pushed as an audit.changed event like any other verdict change.Authorizations
Your secret API key. Create one from your dashboard.
Query Parameters
Chain the contract is deployed on, as a case-insensitive symbol. See Supported Chains. SOLANA (alias SOL) audits a Solana token by its mint address.
ROBINHOOD, BSC, BASE, ETH, AVAX, STABLE, ARC, ARB, HYPE, MONAD, PLASMA, POLYGON, MEGAETH, OP, LINEA, TEMPO, MANTLE, ABSTRACT, SONIC, BLAST, APE, XLAYER, INK, UNICHAIN, STORY, ZKEVM, SCROLL, ZKSYNC, SOLANA The subject to audit: a token address, a liquidity pool (a Uniswap V2/V3 pair address or a Uniswap V4 pool id, 32 bytes), or a Uniswap V4 hook address. A pool resolves to its token: the response's audit.address is that resolved token and audit.pool describes the pool the hook verdict was computed against. A hook returns the hook's own audit: audit.address is the hook and audit.hookContext lists the pools and tokens that use it. audit.entry tells you how it was addressed. Cache your results by audit.chain + audit.address, not by the value you sent. On chain=solana: the token's base58 mint address, CASE-SENSITIVE (send it exactly as written; a lowercased mint is another account). Pool and hook entries are EVM only.
^(?:0x(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})|[1-9A-HJ-NP-Za-km-z]{32,44})$Optional, EVM only. Pin a specific pool for a token entry: address is the token, pool is the pool whose hook drives the verdict (a V2/V3 pair address or a V4 pool id). A mismatch (the pool does not trade the token) is a 400. Not used on chain=solana (it answers 404 pool_not_found): send the mint alone.
^0x(?:[a-fA-F0-9]{40}|[a-fA-F0-9]{64})$Closed-source contracts are audited on decompiled bytecode (the most expensive tier). Set false to opt out: closed-source contracts then answer { "audit": null, "reason": "decompile_disabled" } instead of being decompiled, and cached decompiled verdicts are not served. Sent as the string "true"/"false" in the query.
true, false Async mode. false (default): the request holds until the audit is done, classic synchronous behavior. true: if no cached audit exists, the audit is started in the background and the response is { status }; poll the SAME URL every 1-2s until it flips to { audit } (typically 10-30s for a first-time audit). There is no job id: an audit is idempotent per (chain, address), so the token address is the job handle. Billing is identical in both modes; progress responses are never billed. With subscribe=true you do not even poll: the finished verdict is pushed to you (over your SSE stream or webhook) the moment it lands (see SSE & Webhooks).
true, false Subscribe this (chain, address) to push updates as part of this call. Once subscribed, any later verdict change (safe↔unsafe flip, owner change, a gate or honeypot flipping, or a closed-source token re-audited on its now-verified source) is pushed to you over a live SSE stream or a webhook, instead of you polling. Combined with async=true, the finished audit result is itself pushed when ready, so you fire the call and just consume the events. Subscribing works with or without a delivery transport configured; with neither, pull the changes from GET /api/events (see SSE & Webhooks). Charges the one-time subscribe fee for a genuinely-new token; idempotent for an already-subscribed one.
true, false Response
The audit verdict, wrapped in an audit object.
- Option 1
- Option 2
- Option 3
The response shape.
The audit result.
Show child attributes
Show child attributes
Echo of what this call was billed, present only for authenticated API-key callers (never for anonymous/browser traffic). type is the bill category (e.g. cached, fresh_no_decompile, fresh_with_decompile, refresh) or null when the call is free; credits is the amount metered.
Show child attributes
Show child attributes