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 keyAuthorization: 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.
Bearer routes never redirect. A bad or missing key gets 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

FieldTypeMeaning
addressstring, required40 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.
chainstring, 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.
Billing. We reserve 8 credits and settle 5 if the address turned out to be a plain wallet, refunding the difference to the exact pool it was drawn from. You are never billed above the reserve, and never billed the dearer price for the cheaper answer.

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

idApplies toWeightHow it is read
sanctionsany address34Membership 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.
identityany address16eth_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_verifiedcontract22Sourcify 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.
upgradeablecontract14eth_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.
metadatatoken8name(), symbol(), decimals() and totalSupply() read directly with eth_call.
privilegestoken16Role 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_transfertoken14A 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.

A getter that answers with a well-formed, non-zero address is real evidence: a contract does not name an account by accident. Absence is not establishable with the reads available, so it is returned as 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.

idWhy not
ageNeeds 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_countSame paywall, same silent failure. Absent rather than approximated.
holder_concentrationNeeds 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.
liquidityReading pool depth honestly means indexing every venue on the chain. No keyless source, so no number.
reputationNo 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_privilegesStructurally 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.
auditNot on-chain. Verified source is not an audit and is never presented as one.
honeypotWe 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.
The first four all trace to one measurement: the block-explorer API refuses free access on Base and does it silently — HTTP 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

BandMeaning
clearEvery signal that resolved looked ordinary. Read notChecked before treating that as an all-clear.
cautionSignals were uneven. Read the evidence before sending anything here.
highAt least one signal resolved badly. Do not act on this address without reading why.
insufficientToo 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 14

The 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.

A 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.

A 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, notChecked

MCP

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_…
ToolCostWhat it does
check_address5–8 crScreen one address. Bills the address price or the token price depending on what it turned out to be.
get_address_checkfreeRe-read a check by id, with every signal and its evidence.
list_address_checksfreeThe caller's history, newest first, filterable by band.
get_supported_signalsfree · publicEvery signal, how it is read, and everything we deliberately cannot check with the measured reason. Read this before interpreting a result.
get_credit_balancefreeThe account's spendable Ounie credits.
get_pricingfree · publicCredit prices and the x402 endpoint.
whoamifreeThe 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 a 4xx with 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 a kind: "token" request on something that is not a token returns 402 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

StatusBodyMeaning
400empty · not_hex · wrong_length · bad_checksum · zero_address · unsupported_chain · unsupported_kind · kind_requiredSomething about the request is always fatal. Every one carries a `detail` explaining it. Fix it and retry.
400not_a_tokenYou asked for a token report on something with no ERC-20 surface. Ask for kind `address`.
401unauthorizedNo usable credential. Bearer routes never redirect.
402insufficient_creditsCarries required_credits, balance_credits and buy_credits_url.
402thin_result · not_a_token (x402 only)Caught after verify and before settle. Your payment was not settled.
429too_many_runningMore than 5 checks in flight for one owner.
502check_failedSomething broke. You were not charged.
503payment_not_configuredThe x402 rail is not configured on this deployment.

Limits

LimitValue
ChainsBase mainnet (8453) only
Concurrent checks per owner5
Active API keys per owner5
Price5 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.