Verdict API · v1

One call. A verdict you can prove.

Send an address and a chain. We fork the chain, buy the token and try to sell it, and return what actually happened — plus a signed receipt anyone can verify. HONEYPOT means the sell really reverted, not that a heuristic disliked the bytecode.

QuickstartAuthenticationRate limits RequestResponseErrors ExamplesGetting a keyPricing Limits & caveats
Quickstart

Base URL and your first call

The API is served over HTTPS at:

https://api.omniguardlabs.com

https://check.provewall.com is an equivalent alternate origin — same service, same routes, same keys. Use either. There is no api.provewall.com; if you have it written down somewhere, it will not resolve.

curl -X POST https://api.omniguardlabs.com/v1/verdict \
  -H "X-Api-Key: og_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"address":"0xdAC17F958D2ee523a2206206994597C13D831ec7","chain":"eth"}'

Every endpoint below lives on that base URL.

EndpointAuthWhat it does
POST /v1/verdictAPI keyExecution-truth honeypot / exitability verdict for one token.
GET /v1/usageAPI keyYour plan and this month’s call count.
POST /api/receipt/verifynoneVerify a signed receipt we issued. Keyless on purpose — your users can check us without an account.
GET /healthnone{"ok":true}. For your uptime monitor.
GET /statusnoneWhich detection layers are live or degraded on the serving box, right now.
Authentication

One key, two accepted headers

Send your key as either header. Both work identically; pick one.

X-Api-Key: og_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
— or —
Authorization: Bearer og_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys start with og_live_ and are high-entropy random. We store only a SHA-256 hash of your key — we cannot show it to you again, and a database of ours does not contain a usable key.
Enforcement fails closed. Missing, malformed, unknown or revoked → 401, never a silent pass and never a partial verdict.
Server-side only. Browser calls are blocked by our CORS policy by design. Never put an API key in front-end code — call us from your backend and pass the result down.
Cancel and access stops. Keys are bound to your subscription. If it is cancelled or goes unpaid, the key is revoked and the next call returns 401.

Rotating or lost a key? Reply to your key email or write to support and we will revoke the old one and issue a replacement. Revocation takes effect on the next call.

Rate limits

Per key, per minute and per day

These are the limits configured in the service today. Both windows are sliding, and both are counted per key.

PlanRequests / minuteRequests / day
Dev302,000
Pro12020,000
Enterprise600200,000

Exceeding either window returns 429 with the plan named in the body. There is no monthly call quota on any plan — we meter your usage and show it back to you, but we do not cut you off at a number. If you need a higher ceiling than Enterprise, talk to us.

Request

POST /v1/verdict

POST /v1/verdict
X-Api-Key: og_live_…
Content-Type: application/json

{"address": "0x…", "chain": "eth"}
FieldRequiredNotes
addressyesEVM: 0x + 40 hex characters. Solana: a base58 mint address, 32–44 characters.
chainnoOne of eth, bsc, sol. Aliases ethereum, bnb, solana also work. Defaults to eth. Need Base or another chain? Ask us — the router accepts it but we will confirm it is provisioned for you before you build on it.

Unknown fields are ignored. One token per call.

Response

200 — a freshly proven verdict

Returned when we ran a fork for this call (cached: false). Illustrative shape:

{
  "verdict": "SELLABLE",          // HONEYPOT | SELLABLE | UNVERIFIED
  "proven": true,                 // true only when execution produced this verdict
  "sellable": true,               // the sell succeeded AT THIS BLOCK
  "buy_tax": 0.0,
  "sell_tax": 3.0,
  "tax_tier": "GREEN",
  "severity": { "color": "GREEN", "emoji": "…", "meaning": "…", "label": "…", "sell_tax": 3.0 },
  "owner_powers": ["mint - owner can mint / dilute supply"],
  "owner_powers_status": "known", // known | unavailable
  "block": <int>,                  // block height the fork ran at
  "receipt": { "id": "rc_…", "signed": { … } },
  "heuristics": [ { "name": "owner_power", "level": "mandatory", "note": "…" } ],
  "chain": "eth",
  "address": "0x…",
  "detail": "SELL REVERTED",      // machine-ish reason
  "summary": "…",                 // one sentence, plain English
  "trap_capable": false,
  "reachable_traps": [],
  "cached": false,
  "usage": { "plan": "pro", "used_this_month": 412 }
}
FieldTypeMeaning
verdictstringHONEYPOT — we bought it and the sell reverted or trapped the funds. SELLABLE — the sell went through at this block. UNVERIFIED — we could not execute a proof, so we are not grading it.
provenboolTrue only when execution produced the verdict. Never infer safety from proven: false.
sellableboolThe exit worked at block. This is a fact about one block, not a promise about the future.
buy_tax / sell_taxnumber | nullMeasured percentages. null when not measurable.
tax_tierstring | nullBand for the sell tax: GREEN / YELLOW / ORANGE / RED.
severityobjectcolor (GREEN / YELLOW / ORANGE / RED / GREY), plus a human label and meaning. Derived from the proof, not from a score.
owner_powersstring[]Owner capabilities we found — mint, blacklist, pause, fee switches and similar.
owner_powers_statusstringknown or unavailable. Tells you whether the check ran, so an empty list can never be mistaken for “checked, found nothing”.
blockint | nullBlock height the fork ran at.
receiptobject | null{ id, signed } when the verdict is proven. Verify it yourself at POST /api/receipt/verify — no key needed.
heuristicsobject[]{ name, level, note }. level: "mandatory" marks owner powers you should surface to your user.
chain / addressstringEchoed back.
detailstringShort machine-ish reason, e.g. SELL REVERTED or no Uniswap-V2 route to buy.
summarystringOne-sentence plain-English explanation, safe to show a user.
trap_capable / reachable_trapsbool / arrayWhether the owner can turn an exitable token into a trap after you buy. Surface trap_capable to your user when present.
cachedboolWhether this came from the verdict ledger. Branch on this — see below.
usageobject{ plan, used_this_month }.
degradedstring[]Only present when a check could not run. See “Fail closed” below.
degraded_detailobjectWhy each degraded check could not run.
exit_proven / exit_proofbool / objectPresent alongside degraded: the execution truth we did establish, preserved even when the overall grade was downgraded.

Fail closed: a downgrade is not a clean bill of health

If a control could not run — most often reading owner powers — we do not hand out a green light we did not earn. The execution truth is preserved in exit_proven / exit_proof, but the grade degrades: verdict becomes UNVERIFIED, proven becomes false, severity.color becomes GREY, and degraded_detail says exactly what failed. A proven HONEYPOT is never downgraded this way — softening a proven trap because a side-check failed would be the same mistake in the other direction.

Integration rule: never treat “not HONEYPOT” as safe. Gate on verdict === "SELLABLE" && proven === true, and surface degraded and trap_capable to your user when present.

200 — served from the verdict ledger (cached: true)

We research once and serve forever until the record ages out or a monitored change invalidates it, so you are not paying fork latency for a token we already proved. The freshness window is 24 hours. A cached response is a smaller object — this is the one shape difference you must handle:

{
  "verdict": "SELLABLE", "proven": true, "sellable": true,
  "block": <int>, "receipt_id": "rc_…", "ts": <unix seconds>, "stale": false,
  "chain": "eth", "address": "0x…",
  "trap_capable": false, "reachable_traps": [],
  "receipt": { "id": "rc_…", "signed": { … } },
  "cached": true,
  "usage": { "plan": "pro", "used_this_month": 413 }
}

A cached response has no severity, summary, heuristics, owner_powers, detail, tax_tier or tax fields. ts is the unix time the fork actually ran, so you can always tell how old the underlying proof is. If you need a guaranteed fresh fork on a specific token, tell us — forced re-proving is available and we will wire it to your key.

Errors

Every status this endpoint returns

StatusBodyCause
401{"error":"invalid or revoked API key","verdict":null}Key missing, malformed, unknown, or revoked (including after a cancelled or unpaid subscription).
401{"error":"key lacks required scope: scan:read","verdict":null}Valid key, but scoped away from this endpoint. Contact us to widen it.
429{"error":"rate limit exceeded for plan '<plan>'","verdict":null}Per-minute or per-day window exceeded. Back off and retry; the windows are sliding.
405{"detail":"Method Not Allowed"}Anything other than POST. The allow header names the right one.
400Disallowed CORS originA browser preflight from an origin outside our allowlist. Call from your server instead.

Input errors arrive as 200, not 4xx. A malformed address or an unsupported chain returns 200 with {"error":"invalid address","verdict":"UNVERIFIED", …}. Check for an error key in the body, not only the HTTP status. We consider this wrong and intend to move input validation to 400; it is documented here because it is what the endpoint does today, and we would rather you code against the truth than against our intentions.

Examples

Copy, paste, run

curl

curl -sS -X POST https://api.omniguardlabs.com/v1/verdict \
  -H "X-Api-Key: $OMNIGUARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"address":"0xdAC17F958D2ee523a2206206994597C13D831ec7","chain":"eth"}'

Node.js (server-side fetch)

// Runs on your server. Never ship the key to a browser.
async function verdict(address, chain = "eth") {
  const res = await fetch("https://api.omniguardlabs.com/v1/verdict", {
    method: "POST",
    headers: {
      "X-Api-Key": process.env.OMNIGUARD_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ address, chain }),
  });

  if (res.status === 401) throw new Error("OmniGuard: key rejected");
  if (res.status === 429) throw new Error("OmniGuard: rate limited — back off");

  const data = await res.json();
  if (data.error) throw new Error("OmniGuard: " + data.error);   // input errors come back as 200

  // Fail closed: only "proven SELLABLE, nothing degraded" counts as a pass.
  const safeToTrade =
    data.verdict === "SELLABLE" && data.proven === true &&
    !data.degraded && data.trap_capable !== true;

  return { safeToTrade, verdict: data.verdict, cached: data.cached, raw: data };
}

Python (requests)

import os, requests

BASE = "https://api.omniguardlabs.com"
SESSION = requests.Session()
SESSION.headers.update({"X-Api-Key": os.environ["OMNIGUARD_API_KEY"],
                        "Content-Type": "application/json"})

def verdict(address: str, chain: str = "eth") -> dict:
    r = SESSION.post(f"{BASE}/v1/verdict",
                     json={"address": address, "chain": chain}, timeout=120)
    if r.status_code == 401:
        raise RuntimeError("OmniGuard: key rejected")
    if r.status_code == 429:
        raise RuntimeError("OmniGuard: rate limited - back off and retry")
    r.raise_for_status()

    data = r.json()
    if "error" in data:                      # input errors come back as HTTP 200
        raise ValueError(f"OmniGuard: {data['error']}")
    return data

v = verdict("0xdAC17F958D2ee523a2206206994597C13D831ec7", "eth")

# Fail closed. "Not HONEYPOT" is not the same thing as safe.
ok = (v["verdict"] == "SELLABLE" and v["proven"] and
      not v.get("degraded") and not v.get("trap_capable"))

# A cached record is a smaller object - only the fresh shape carries `summary`.
print(v["verdict"], "| cached:", v["cached"], "|", v.get("summary", "(cached record)"))

Checking your usage

curl -sS https://api.omniguardlabs.com/v1/usage -H "X-Api-Key: $OMNIGUARD_API_KEY"
 {"plan":"pro","prefix":"og_live_a1b2","used_this_month":413}

Verifying a receipt (no key required)

curl -sS -X POST https://api.omniguardlabs.com/api/receipt/verify \
  -H "Content-Type: application/json" \
  -d '{"receipt": { … the signed object from a verdict … }}'
 {"valid":true}

This one is deliberately keyless: your users can check our proof without an account with us.

Getting a key

How you actually get access

Being straight with you about this, because you are buying a developer product and need to know whether a human is in the loop:

1. Subscribe to a tier below. Standard Stripe checkout; use a work email you can receive at, because that is where the key is sent.
2. We mint a key bound to your subscription, scoped to the Verdict API. Minting is automated off the payment event — there is no form to fill in and no contract to submit.
3. You get one email containing the key in plaintext, exactly once. We only ever store a hash of it, so we cannot resend the same key — save it in your secret manager immediately.
4. Expect it within one business day. It is usually much faster, but we would rather under-promise. If it has not arrived, check spam and then email us and a human will sort it out immediately.

Your first email after checkout is a generic thank-you for the subscription. The key arrives in a separate email titled “Your OmniGuard API key”. If you only ever see the first one, write to us — do not assume it is still processing.

Evaluating before you buy, or need an invoice, a DPA, a specific chain, or a higher ceiling? Talk to us first — we would rather scope it properly than take a subscription that does not fit.

Pricing

Three tiers, priced by throughput

All three tiers return the same verdict, with the same proof and the same signed receipt. What you are buying as you move up is throughput, not a better answer. We would rather say that plainly than imply a capability ladder that does not exist.

Dev

$199 / mo

30 requests / minute
2,000 requests / day

Subscribe →

Pro

$799 / mo

120 requests / minute
20,000 requests / day

Subscribe →

Enterprise

$2,500 / mo

600 requests / minute
200,000 requests / day

Subscribe →

Buying Enterprise for the volume alone works, but if you want it for anything else — an SLA, dedicated capacity, a private hostname, procurement paperwork — speak to us first so we can put it in writing rather than have you infer it from a price.

Limits & caveats

What this API does not do

SELLABLE is not “safe”. It means the exit executed at one specific block. An owner with the right switches can change that in the next block — which is precisely what trap_capable is warning you about.
UNVERIFIED is honest, not lazy. No liquidity route, no fork, no proof. We refuse to grade what we could not execute, so an UNVERIFIED is never quietly counted as a pass.
Verdicts can be up to 24 hours old. The ledger serves a proven token without re-forking until the record ages out or a monitored change invalidates it. cached and ts always tell you which you got.
Server-side only. CORS is locked to our own origins. This is deliberate: the correct place for an API key is your backend.
Rate limits are per serving process and held in memory, so a deploy resets the windows. Do not build a design that depends on a limit being enforced across a restart.
One token per call. No batch endpoint yet. If you need bulk screening, tell us what volume and shape and we will talk.

Something in these docs not matching what you observe? That is a bug and we want to hear about it — tell us and we will fix the endpoint or the page, whichever is wrong.