methodology: every rule that produces a verdict
00

What is published here, and why

no trust required

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

01

The facts a verdict is derived from

AnalyzerInput

Every 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).

isNativeB20 / isCopycatchain read  The B20 factory precompile is ground truth: 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.
variantchain read  Encoded in address byte 10: 0x00 asset, 0x01 stablecoin. No RPC needed.
name / symbolchain read  Token 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.
powerschain read  Role-holder counts from a full replay of 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.
seizeDedicated / pauseDedicatedchain read  Whether a dedicated BURN_BLOCKED_ROLE / PAUSE_ROLE holder exists right now, beyond the admin's latent ability to appoint one.
freezeConfiguredchain read  Whether a transfer blocklist policy is enforced at the pinned block (policy id set in the PolicyRegistry), meaning holders can be frozen out of transfers now.
activitychain read  Counts of issuer actions since creation: mints, burns, seizures, pauses, supply-cap changes, policy changes, and the block of the last such action.
creatorNoncechain read  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).
viaLauncher / launcherchain read  From creation provenance: whether the factory was called through an intermediary launcher contract rather than directly, in which case the on-chain creator is the launcher's caller, not the transaction initiator.
canonicalAssetchain read  Strict contract-address match against a frozen 25-entry allowlist of canonical Base deployments (USDC, WETH, cbBTC and so on), listed in the served knownAssets.ts. Matched by address only, never by name or symbol.
hasCodechain read  Whether the address has deployed contract code at all. A codeless address (EOA, undeployed) legitimately has no readable symbol, so it is exempt from SYMBOL_UNREADABLE.

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

Every flag, its exact trigger

15 codes

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

flagseverityfires whenbasis
COPYCAT_NON_NATIVEhighThe 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_IMPOSTERhighThe 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_ACTIVEhighA 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_UNREADABLEmediumsymbol() 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_LOOKALIKEmediumThe folded ASCII symbol is exactly 1 edit away from a well-known ticker: possible typosquat.heuristic
CAN_SEIZEmediumA 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_MINTmediumSupply 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_ACTIVEmediumA 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_PAUSElowA dedicated PAUSE_ROLE holder exists: transfers, mints and burns can be halted.chain read
TRANSFER_RESTRICTEDlowTransfers are gated by an allow/blocklist policy: holders may be unable to move tokens freely.chain read
FRESH_CREATORlowThe creator address has a transaction count of 3 or less at the pinned block: newly-created wallet.heuristic
CANONICAL_ASSETinfoThe 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_B20infoThe address is not a native B20 token (no factory record) and not a copycat either. Out of scope, stated as such.chain read
ZERO_ACTIVITYinfoNo issuer action observed since creation: no mints, pauses, seizures or role changes.chain read
VIA_LAUNCHERinfoThe 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
03

How flags roll up into a verdict

deriveVerdict

The 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: false remains 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.
04

What happens when a check cannot run

fail-closed

The 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 explicit NOT_INDEXED flag, 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 (hasCode defaults to true, canonicalAsset to absent), so a caller that forgets a fact gets extra flags, never a cleared scam.
05

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.