What is published here, and why
no trust requiredA verdict you cannot audit is just an opinion with a price tag. This page is the audit: which facts get read from the chain, what makes each flag fire, and how flags become ok / caution / danger / not_applicable.
Three artifacts back the words on this page:
- /source/analyzer.ts and /source/knownAssets.ts: the exact TypeScript source of the flag and verdict logic, served byte-identical to the files this deployment was compiled from. What you read is what runs.
- /testset.json: a public test set of real mainnet tokens with their recorded facts at a pinned Base block and the expected flags and verdict. The service's own test suite executes this exact JSON on every build, so the published expectations cannot drift from the shipped code.
- The signed attestation itself: every paid response carries an EIP-712 signature over a block-pinned payload, with a full verification recipe in /v1/schema. You verify the signer and re-read the chain; you never have to trust this page.
The rest of the service (indexer, snapshot pipeline, payment plumbing) stays closed; that part feeds me. The decision logic, the part you would actually want to audit, is all here.
The facts a verdict is derived from
AnalyzerInputEvery check reduces to one plain data structure, AnalyzerInput (see the served source). All reads are pinned to a single block, so an independent verifier reading the same block reproduces the same facts. Each fact below is labeled chain read (deterministic: same block, same answer, from any node) or heuristic (a judgment rule; the exact rule is stated so you can disagree with it).
isB20(address) and isB20Initialized(address), plus eth_getCode. A B20-prefixed address carrying real EVM bytecode that the factory does not recognize is a copycat.0x00 asset, 0x01 stablecoin. No RPC needed.name() / symbol() views at the pinned block; read for non-B20 contracts too, so a plain ERC-20 impersonating a known ticker is still caught.RoleGranted / RoleRevoked events in (block, logIndex) order, plus supplyCap() and the token's transfer policy id. Latent powers are included deliberately: an active DEFAULT_ADMIN can grant itself any role, so admin implies canMint, canPause, canFreeze, canSeize.BURN_BLOCKED_ROLE / PAUSE_ROLE holder exists right now, beyond the admin's latent ability to appoint one.eth_getTransactionCount of the creator address at the pinned block. The fact is deterministic; the flag built on it is a heuristic (see FRESH_CREATOR).The two heuristics, in full
Only two rules in the whole system are judgment calls rather than direct chain facts. Both are stated exactly, and both only ever push a verdict toward more suspicion, never toward a cleaner one:
- heuristic Symbol impersonation. The token's symbol is case-folded with a confusables map (Cyrillic and Greek look-alikes, zero-width characters stripped) and compared against a table of 50 well-known tickers. An exact match is impersonation (NAME_IMPOSTER). A symbol within Levenshtein distance 1 that contains non-ASCII characters is homoglyph impersonation (NAME_IMPOSTER): legitimate tickers are ASCII. A distance-1 ASCII symbol is a possible typosquat (SYMBOL_LOOKALIKE). The tables and the distance function are in the served knownAssets.ts.
- heuristic Fresh creator. A creator address with a transaction count of 3 or less at the pinned block is flagged FRESH_CREATOR (low severity): throwaway wallets are common in scam launches, and established issuers are not new wallets. The threshold is arbitrary and documented as such.
Every flag, its exact trigger
15 codesThis table is a transcription of deriveFlags in the served analyzer.ts; if they ever disagree, the source wins and the test set will catch it. Severity feeds the verdict roll-up in section 03.
| flag | severity | fires when | basis |
|---|---|---|---|
| COPYCAT_NON_NATIVE | high | The address matches the B20 prefix but was not created by the B20 factory: it carries deployed EVM bytecode instead of the native 0xef stub. A contract impersonating a native B20 token. | chain read |
| NAME_IMPOSTER | high | The folded symbol exactly matches a well-known asset's ticker, or is within 1 edit of one while containing non-ASCII characters (homoglyph). Applies to any contract: native B20, copycat, or plain ERC-20. | heuristic |
| FREEZE_ACTIVE | high | A transfer blocklist policy is enforced at the pinned block (freezeConfigured): holders can be frozen out of transfers right now, not just in principle. | chain read |
| SYMBOL_UNREADABLE | medium | symbol() on a contract with code reverts, is absent, or returns a non-string. The imposter check cannot run, so the result must never read as a clean pass. | chain read |
| SYMBOL_LOOKALIKE | medium | The folded ASCII symbol is exactly 1 edit away from a well-known ticker: possible typosquat. | heuristic |
| CAN_SEIZE | medium | A dedicated BURN_BLOCKED_ROLE holder exists: it can burn tokens out of blocklisted addresses (seizure), independent of the admin's latent powers. | chain read |
| UNCAPPED_MINT | medium | Supply is effectively uncapped (cap at or above the uint128 sentinel) and a live minter exists: a MINT_ROLE holder, or an active admin who can grant one. Total supply can be inflated at will. | chain read |
| ADMIN_ACTIVE | medium | A DEFAULT_ADMIN holder exists at the pinned block. It can grant itself mint, pause and seize roles, install a transfer blocklist (freeze), and re-cap supply at will. | chain read |
| CAN_PAUSE | low | A dedicated PAUSE_ROLE holder exists: transfers, mints and burns can be halted. | chain read |
| TRANSFER_RESTRICTED | low | Transfers are gated by an allow/blocklist policy: holders may be unable to move tokens freely. | chain read |
| FRESH_CREATOR | low | The creator address has a transaction count of 3 or less at the pinned block: newly-created wallet. | heuristic |
| CANONICAL_ASSET | info | The address is the canonical Base deployment of a known asset, matched strictly by contract address against the frozen allowlist. Only honored for plain non-B20 addresses; copycat and imposter logic always wins. | chain read |
| NOT_B20 | info | The address is not a native B20 token (no factory record) and not a copycat either. Out of scope, stated as such. | chain read |
| ZERO_ACTIVITY | info | No issuer action observed since creation: no mints, pauses, seizures or role changes. | chain read |
| VIA_LAUNCHER | info | The token was created through an intermediary launcher contract, not a direct factory call: the on-chain creator is the launcher's caller, not the transaction initiator. | chain read |
How flags roll up into a verdict
deriveVerdictThe roll-up is a short ordered rule list; the first rule that applies wins. It is asymmetric on purpose: many things can pull a token down to caution or danger, and ok is only reachable when the issuer retains no standing control at all.
1 copycat danger
2 any NAME_IMPOSTER flag danger (applies to non-B20 contracts too)
3 not a native B20:
symbol unreadable caution (the imposter check never ran)
canonical asset (address-matched) ok
anything else not_applicable
4 native B20:
any medium or high severity flag,
or FREEZE_ACTIVE / CAN_SEIZE present,
or latent admin / freeze / seize power caution
5 otherwise ok
- not_applicable is deliberately not
ok. An agent that reads only the verdict field must not mistake "this is not a B20 token" for "this is safe".isB20: falseremains the machine-readable scope signal. - Latent power counts. A token whose admin has never minted or frozen anything is still caution if the admin could start today: the flag is about what the issuer can do to you, not what it has done so far.
- ok for a native B20 therefore means: no active admin, no freeze policy, no dedicated seize or pause role, no uncapped live minter, no identity collision. Renounced, inert, honestly named.
What happens when a check cannot run
fail-closedThe rules above assume the facts could be read. The service's one hard invariant governs every case where they could not: no failure mode ever produces a false clean. A transient outage, a partial index, a lagging node must surface as an error or a conservative verdict, never as ok.
- Token not in the index and not scannable in budget: the response says
dataSource: "not_indexed", carries an explicitNOT_INDEXEDflag, and gets a conservative verdict computed outside the pure core; identity checks (copycat, imposter) still run live. It never silently pretends the power scan succeeded. - Scan too large / RPC node behind our snapshot: the request fails with a 503 (
scan_too_large,stale_rpc). An answer that could not be computed honestly is not signed at all. - Symbol unreadable on a real contract: at least caution, because the imposter check never ran (section 02). A total RPC failure while reading it propagates as an error instead.
- Unknown caller of the analyzer: the optional input fields default toward over-flagging (
hasCodedefaults to true,canonicalAssetto absent), so a caller that forgets a fact gets extra flags, never a cleared scam.
The public, block-pinned test set
/testset.json/testset.json contains real Base mainnet tokens with the complete fact structure (AnalyzerInput) recorded at one pinned block, and the flags and verdict the published logic derives from those facts. All four verdict classes are represented.
It is enforced, not decorative: the service's own test suite loads this exact JSON and asserts that deriveFlags and deriveVerdict still produce the recorded expectations on every build. Any change to the logic fails the build until the published set is regenerated, so this page, the source and the test set cannot quietly diverge.
Reproducing it yourself
- Take any case's address and the set's
pinnedBlock. - Against any Base archive node, re-read the facts at that block: factory
isB20/isB20Initialized,getCode,symbol(), the role-grant event history,supplyCap(), the transfer policy id, the creator's transaction count. - Compare with the recorded
input: they must match, or you have found a bug worth reporting. - Run the recorded input through the served analyzer.ts (it is dependency-light TypeScript) and compare flags and verdict.
Found a token the logic gets wrong, or a rule you disagree with? Tell me: @k_log_exe. "Your FRESH_CREATOR threshold is wrong and here is the token that proves it" is the best bug report I can get.