API reference
Chain Check API
Post an address on Base, get every signal we can read about it — each with the observation it came from — plus the list of everything we refused to guess. One synchronous call, JSON in and out. Three rails share one meter: 5 Ounie credits an address and 8 for a token contract on REST and MCP, $0.060 and $0.096 keyless over x402.
Quickstart
curl -X POST https://onchain.ounie.com/api/checks \
-H "Authorization: Bearer chn_live_…" \
-H "Content-Type: application/json" \
-d '{
"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"chain": "base",
"kind": "auto"
}'The whole result comes back on the same request — typically in two to five seconds, since a token check batches around twenty chain reads plus two keyless HTTP lookups. There is no job to poll.
Authentication
Three credentials are accepted, in this order:
- Your app key —
Authorization: Bearer chn_live_…, minted at /dashboard/api-keys. Up to 5 active. Only the sha256 hash is stored; the raw token is shown once. - The fleet master key — your ounie.com developer key (
ounie_live_…), if you have turned on “Use across Ounie apps” in your Ounie settings. One key, the whole fleet. - The shared session cookie — what the dashboard uses. Same routes, same meter.
401, not a 302 to a sign-in page — a redirect there breaks every non-browser caller silently.Hosts that cannot set an Authorization header (the Ounie AI Team’s manual MCP entry, for one) can pass ?api_key=chn_live_… instead.
POST /api/checks
| Field | Type | Meaning |
|---|---|---|
| address | string, required | 40 hex characters, with or without 0x. A mixed-case address is treated as an EIP-55 checksum and is refused if it does not verify — that means a character was transposed in transit, and reporting confidently on the wrong address is worse than refusing. All-lowercase asserts no checksum and is accepted as-is. |
| chain | string, default "base" | Base mainnet only. Every reader here was verified against Base; anything else is refused rather than attempted. |
| kind | "auto" | "address" | "token" | auto resolves it from the address itself and bills whichever it turned out to be. token on something without an ERC-20 surface is refused, not fabricated. |
The response
{
"ok": true,
"check": {
"id": "…",
"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"chain": "base",
"checked": "token", // what it resolved to
"kind": "contract", // eoa | eoa_delegated | contract
"delegate": null, // set only for eoa_delegated
"band": "clear", // clear | caution | high | insufficient
"band_meaning": "…",
"score": 88, // null iff band is insufficient
"resolved_signals": 6,
"resolved_weight": 108,
"token": { "name": "USD Coin", "symbol": "USDC",
"decimals": 6, "totalSupply": "…",
"totalSupplyFormatted": "…" },
"proxy": { "standard": "ZeppelinOS implementation",
"slot": "0x7050…f8c3",
"implementation": "0x2ce6…d779" },
"privileged_roles": [
{ "getter": "pauser()", "label": "Pauser — can halt transfers",
"account": "0x…" }
],
"signals": [
{ "id": "sanctions", "label": "Sanctions list",
"status": "pass", // pass | warn | fail | insufficient
"value": 1, "weight": 34,
"evidence": "Not present among the … addresses on the OFAC SDN list …",
"source": "https://…" }
],
"not_checked": [
{ "id": "age", "label": "Address age", "reason": "…" }
],
"thin": false,
"credits_spent": 8,
"disclaimer": "…"
}
}Every signal carries its own evidence and source. That is the contract: you should be able to go and repeat any observation we made, and recompute the band yourself from the array.
The signals
| id | Applies to | Weight | How it is read |
|---|---|---|---|
| sanctions | any address | 34 | Membership of the OFAC SDN digital-currency address list, fetched fresh and validated for length and shape before it is allowed to answer.OFAC publishes no BASE tag, so a Base address is screened against the ETH-tagged entries. Correct practice, and an inference. |
| identity | any address | 16 | eth_getCode, read three ways: empty is a plain wallet, an 0xef0100 prefix is an EIP-7702 wallet that delegates to another contract, anything else is contract code. |
| source_verified | contract | 22 | Sourcify v2 for chain 8453.Verified means the published source compiles to the deployed bytecode. It is not an audit and says nothing about whether the code is safe. |
| upgradeable | contract | 14 | eth_getStorageAt across 5 standard proxy slots (EIP-1967, EIP-1822, ZeppelinOS, beacon, Safe singleton) plus both admin slots.Nothing found means nothing in the STANDARD places. A bespoke proxy keeping its pointer elsewhere would not appear. |
| metadata | token | 8 | name(), symbol(), decimals() and totalSupply() read directly with eth_call. |
| privileges | token | 16 | Role getters (owner, pauser, blacklister, master minter, minter, rescuer, governance, timelock) plus AccessControl member counts. A getter that answers with a live account is proof that role exists.PRESENCE ONLY. This signal can never come back clean: an empty return means 'no such function' on one contract, 'a fallback swallowed it' on another, and 'no code here' on a third. We will tell you a token HAS a pauser. We will never tell you it has none. |
| fee_on_transfer | token | 14 | A real transfer, simulated with an eth_call state override, measuring what the recipient actually receives. Verified against a synthetic 10% fee token as well as against a clean one.Measures the plain wallet-to-wallet path at this moment. A token that taxes only sells, only pair routes, or only above a threshold would still come back clean. |
Two of these are worth reading twice. upgradeable sweeps 5 standard proxy slots rather than one, because probing EIP-1967 alone reports USDC as non-upgradeable — and USDC is a proxy, with its pointer in the older ZeppelinOS slot. And privileges can never come back clean; see the next section.
Presence, never absence
This API will tell you a token has a pauser, a blacklister or a minter. It will never tell you it has none.
We probed a nonsense function selector against three things on Base. USDC reverted. WETH returned 0x. A plain wallet also returned 0x. So an empty return means “no such function” on one contract, “a catch-all fallback swallowed the call” on another, and “there is no code at this address” on a third — and real functions revert on malformed calldata, so a revert proves nothing either.
status: "insufficient", listed under not_checked, and enforced in code — a pass on this signal throws before the response is built.notChecked
Every response carries a not_checked array. It is not a footnote: an agent that does not know what was skipped will read an absent field as a clean one, which is the difference between a signal and a lie.
| id | Why not |
|---|---|
| age | Needs an indexed transaction history. Etherscan/Basescan V2 refuses free access on Base and does it SILENTLY — HTTP 200 with a body reading {"status":"0","message":"NOTOK"}, so a client checking res.ok reads the refusal as data. We hold no paid explorer key. Rather than estimate it, we do not have it. |
| tx_count | Same paywall, same silent failure. Absent rather than approximated. |
| holder_concentration | Needs the full holder set, which is an explorer or an indexer. There is no keyless route to it, so no top-holder percentage is reported. |
| liquidity | Reading pool depth honestly means indexing every venue on the chain. No keyless source, so no number. |
| reputation | No free, machine-readable scam-report feed is usable here. There is no reputation signal beyond OFAC membership, and inventing one would be worse than having none. |
| absence_of_privileges | Structurally impossible with the reads available. See the privileges signal: an empty eth_call return is produced by three different situations, one of which is a mistyped address. |
| audit | Not on-chain. Verified source is not an audit and is never presented as one. |
| honeypot | We simulate a transfer and report what it delivered. We do not simulate a buy-then-sell across a liquidity venue, so we do not issue a honeypot verdict — the fee-on-transfer signal is the honest subset of that question. |
200 with a body reading {"status":"0","message":"NOTOK"}. A client that checks whether the response was OK reads the refusal as data. We hold no paid explorer key, so we do not have those fields, and we do not estimate them.Bands & scoring
| Band | Meaning |
|---|---|
| clear | Every signal that resolved looked ordinary. Read notChecked before treating that as an all-clear. |
| caution | Signals were uneven. Read the evidence before sending anything here. |
| high | At least one signal resolved badly. Do not act on this address without reading why. |
| insufficient | Too few signals resolved to publish a band. |
score = 100 × Σ(value × weight) ÷ Σ(weight) // resolved signals only
band = score ≥ 75 → clear
score ≥ 45 → caution
otherwise → high
any signal with status "fail" → high, regardless of score
too little resolved → insufficient, score null
weights: sanctions 34, identity 16, source_verified 22, upgradeable 14, metadata 8, privileges 16, fee_on_transfer 14The rule that matters most: a signal we could not observe leaves the denominator. It is never scored as a zero, because that would punish an address for our blind spot and would quietly change what the number means every time an upstream wobbled. A band is always a band of the signals that resolved, and resolved_signals and resolved_weight tell you how many that was.
clear band means every signal that resolved looked ordinary — nothing more. This is a diligence signal, never a verdict on a counterparty, never a security audit, and never financial advice.Thin results & refunds
A check is thin only when the chain would not answer at all — we observed nothing, so there is nothing to sell. It refunds in full and credits_spent is 0.
high band is not thin. It is the most valuable answer this API gives, and it bills normally. So is insufficient on an address whose signals genuinely could not be established — that is a real finding about a real address.Reading past checks
Free forever, on any rail.
GET /api/checks?limit=20&band=caution
GET /api/checks/<id>
GET /api/pricing # public: prices, the whole signal catalogue, notCheckedMCP
Endpoint https://onchain.ounie.com/api/mcp (legacy SSE: /api/sse)
Auth Authorization: Bearer chn_live_…
…or https://onchain.ounie.com/api/mcp?api_key=chn_live_…| Tool | Cost | What it does |
|---|---|---|
| check_address | 5–8 cr | Screen one address. Bills the address price or the token price depending on what it turned out to be. |
| get_address_check | free | Re-read a check by id, with every signal and its evidence. |
| list_address_checks | free | The caller's history, newest first, filterable by band. |
| get_supported_signals | free · public | Every signal, how it is read, and everything we deliberately cannot check with the measured reason. Read this before interpreting a result. |
| get_credit_balance | free | The account's spendable Ounie credits. |
| get_pricing | free · public | Credit prices and the x402 endpoint. |
| whoami | free | The authenticated key's owner metadata. |
get_supported_signals is public and exists for a reason: an agent that does not know a field was never checked will read its absence as a pass.
x402 — keyless, pay per call
Agents with no Ounie account pay in USDC on Base. The caller here is already a wallet holding USDC on Base, which makes this the one endpoint whose audience is exactly its payment method. Two gates protect the payer, and the order is deliberate.
- Before quoting. Anything wrong with the request itself — a malformed address, a failed EIP-55 checksum, an unsupported chain, a missing
kind— returns a4xxwith no price in the body. You are never asked to sign an authorization for work that cannot succeed. - Before settling. The signature is verified off-chain, the check runs, and only then does the money move. A check the chain would not answer returns
402 thin_result, and akind: "token"request on something that is not a token returns402 not_a_token— neither is ever settled. An on-chain settlement is final, so the refusal has to come first.
kind is required here and auto is refused. The two kinds cost different amounts, you sign an authorization for one exact figure, and the exact scheme compares that figure against requirements we rebuild at settle time — so a price we could only learn by doing the work would either be guessed high or risk a settlement refused after the work was done. auto lives on the credit rail, which can refund.curl -X POST https://onchain.ounie.com/api/x402/check \
-H "Content-Type: application/json" \
-d '{"address":"0x8335…2913","kind":"token"}'
# → 402 { "x402Version": 1, "accepts": [{
# "scheme": "exact", "network": "base",
# "maxAmountRequired": "96000",
# "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
# "payTo": "0x…",
# "extra": { "name": "USD Coin", "version": "2" } }] }
curl -X POST https://onchain.ounie.com/api/x402/check \
-H "X-Payment: <base64 signed payload>" \
-H "Content-Type: application/json" \
-d '{"address":"0x8335…2913","kind":"token"}'$0.060 an address, $0.096 a token contract. The network is the bare base, not a CAIP-2 id — the naming scheme selects the protocol version, and a v1 offer must use the bare name. The exact scheme is an offline EIP-3009 authorization, so the facilitator pays the gas and a wallet holding only USDC can pay.
Errors
| Status | Body | Meaning |
|---|---|---|
| 400 | empty · not_hex · wrong_length · bad_checksum · zero_address · unsupported_chain · unsupported_kind · kind_required | Something about the request is always fatal. Every one carries a `detail` explaining it. Fix it and retry. |
| 400 | not_a_token | You asked for a token report on something with no ERC-20 surface. Ask for kind `address`. |
| 401 | unauthorized | No usable credential. Bearer routes never redirect. |
| 402 | insufficient_credits | Carries required_credits, balance_credits and buy_credits_url. |
| 402 | thin_result · not_a_token (x402 only) | Caught after verify and before settle. Your payment was not settled. |
| 429 | too_many_running | More than 5 checks in flight for one owner. |
| 502 | check_failed | Something broke. You were not charged. |
| 503 | payment_not_configured | The x402 rail is not configured on this deployment. |
Limits
| Limit | Value |
|---|---|
| Chains | Base mainnet (8453) only |
| Concurrent checks per owner | 5 |
| Active API keys per owner | 5 |
| Price | 5 cr address · 8 cr token · $0.060 / $0.096 on x402 |
Credits are shared across every Ounie app and bought at ounie.com/dashboard/settings.