---
name: techscan
description: The fast parallel "did anyone actually build this" probe for a project website - what artifacts prove build effort, which probes are 1 second and which are 12, and the catch-all 200 trap that makes naive endpoint probing worthless. Read before any tech-stack or product-reality check on a token's site.
sources: [cowork]
aliases: [tech probe, techscan, build effort, is it vibe coded, route scan, site depth]
---
# RUN THIS FIRST. EVERY TIME. NO EXCEPTIONS.

```
ssh -i ~/kookwatch/id_kookwatch root@159.223.88.168 'python3 /opt/casig/claimcheck.py <site>'
```
(also `claimcheck <site>` on the droplet). Built 13 Sep 2026 **because a hand-run check cost Ben real money.** Do not hand-roll this check again; run the script, then read its output.

## THE PRE-DELIVERY GATE. A verdict is BLOCKED unless all five pass.
1. **Every chunk fetched.** The script pulls the whole `_buildManifest` chunk list. A partial scan is how the decisive evidence gets missed.
2. **The project's OWN contract registry extracted, and every hard claim resolved by `eth_getCode`** with control addresses in the SAME batch. If the controls do not answer, the result is UNRESOLVED, never "empty".
3. **For every live endpoint, the call site was read BEFORE the test was chosen.** Parameterised -> garbage-input control. No input -> **the only valid test is comparing its VALUES to ground truth.** Never call data fake without that comparison.
4. **A nonsense-path control was run.** A site that answers 200 to any path makes every endpoint result meaningless.
5. **Nothing is UNRESOLVED.** If a decisive check could not run, the output is "I could not settle X, here is what I tried", never a label.
6. **Liveness first.** A hosting-platform error page (railway "Application not found", vercel DEPLOYMENT_NOT_FOUND, a 402 payment wall) is **SITE DEAD**, a verdict, not a failed scan. This alone disqualifies 1 project in 5. Never report it as "could not scan".
7. **Depth is text + headings + buttons + routes, never routes alone.** A single-page app has zero hrefs and can still be a real product; a poster has zero of everything. Zero routes on its own has already produced one wrong call.
8. **Chain-awareness before any address is judged.** 11 of 39 live sites in the corpus are Solana apps. An address is only "does not exist" when the site actually targets the chain you queried.
9. **Does the site name THIS token?** page / bundle / nowhere. Half of a fresh 10-project batch never printed their own contract, including the deepest site in it. "Nowhere" downgrades every other score, because none of that work is provably about this coin.
10. **An address must be a whole hex word.** `0x[0-9a-fA-F]{40}` matches inside deploy bytecode; `0x608060405234801561001057600080fd5b506040` is the EVM prologue, not a contract. Use the bounded regex or you will accuse a real project of publishing fake addresses.

Batch harness for a whole corpus: `python3 /opt/casig/batch.py /opt/casig/runN.json` over `/opt/casig/sites.json` (built by `build_corpus.py`). Per-site deadline, incremental writes, structured JSON so an iteration is a diff of two files.

## What the script separates, and why it matters
- **HARD claim** = the project asserting deployment (a `.sol` filename, or a status string like MAINNET ONLINE). Only these can be FALSE.
- **SOFT reference** = a bare `key: "0x.."` config map, which often points at other chains. Reported as "referenced, not asserted here", never as a false claim. **Without this split the tool cries wolf and gets muted:** pairex.market read 15 of 21 FALSE before the fix, and is fine after it.

## Verified against the case that failed, plus controls (13 Sep 2026)
| site | result |
|---|---|
| orionisderivative.tech | **12 of 12 hard claims FALSE**, every published contract an empty address |
| pairex.market | 6 CONFIRMED (usdg, weth, 4 uniswap v3 contracts), 15 soft references, no false alarm |
| railx.dev | no registry published -> **BLOCKED**, "cannot say the protocol is or is not deployed" |

Built and measured 13 Sep 2026, on the droplet (`/root/techprobe.py`, `/root/routes.py`, `/opt/casig/claimcheck.py`). Feeds question 4 and question 9 of Ben's ten DD questions, and the RENTED vs OWNED split. Companion to [[casig-gmgn-overlay]].

# The ask
- [stated] Ben: "How quickly can you check the tech that is on a website... I don't have 10 minutes for these checks... We want to avoid copies. We want to avoid stuff that is hacked together in a couple of hours or in 1 or 2 days. We're looking for things that required a long time to build, that really good people built, that make sense, and that work."

# MEASURED SPEED, and it settles the question
- **Route scan across 5 sites: 1.0 second wall time, all in parallel.** That is the whole Tier 1.
- Full 14-probe scan across 6 sites: **14.6s wall**, but that is set entirely by two slow sources. **crt.sh returned nothing at all** on every site and **archive.org took most of the rest.** Drop both and the deep scan is ~4s.
- **RULE: fan out, never chain. Wall time must equal the slowest probe, not the sum.** ThreadPoolExecutor with a per-probe deadline, report whatever came back, never block on a straggler.

# THE TRAP THAT MAKES NAIVE PROBING USELESS
- **A single-page app answers HTTP 200 for ANY path.** astralborn.com returned 200 for `/api/health`, `/api/stats`, `/api/config`, `/openapi.json` and `/.well-known/security.txt` and looked like the deepest backend in the set. It was a catch-all: a request for `/zzz-does-not-exist-<timestamp>` returned 200 with a body the same size as the homepage.
- **So every endpoint probe must be paired with a nonsense-path control**, and a 200 only counts when the body differs from the homepage AND parses as JSON. Without that control the probe scores landing pages as products.

# WHAT ACTUALLY PROVES BUILD EFFORT, cheapest first
1. **The route list.** Next.js exposes every page at `/_next/static/<buildId>/_buildManifest.js`; otherwise scrape same-origin hrefs. This one field separates an app from a poster:
   - railx.dev: 8 routes including `/api/v0/health`, `/api/v0/openapi`, `/docs`, `/sign-in`, `/sign-up` (auth + a published API spec)
   - ocra.markets: 12 feature routes, `/scanner` `/methodology` `/compare` `/portfolio` `/watchlist` `/docs`
   - prm.market: 9 routes, `/auctions` `/launch` `/rewards` `/status` `/how-it-works` `/docs`
   - pairex.market: 60 routes but **58 of them are `/c/0x...` token pages**, so it is ONE template repeated. Count DISTINCT PAGE SHAPES, never raw route count.
   - astralborn.com: 0 routes, catch-all.
2. **Auth, docs, status, terms.** A sign-in flow, an openapi/docs page, a status page and real terms cost days each and a weekend build has none of them.
3. **Source maps.** When present they hand over the original file tree and the hand-written file count, which is the closest thing to a build-hours meter. **Not present on any of these six** (Next.js strips them in production), so treat it as a bonus, not the plan.
4. **Vendor hosts in the page.** Rented vs owned, straight from the HTML. pairex.market calls blockscout, pinata, flagcdn, tcgplayer; railx.dev and ocra.markets call almost nothing outside themselves.
5. **GitHub first commit, commit count, contributors.** The gold standard for elapsed build time. Blocked from the Cowork container, fine from the droplet.

# THE FINDING THAT CHANGES THE WHOLE APPROACH
**Domain age cannot separate these projects, because they are all newborn.** RDAP registration dates, measured 13 Sep 2026: ocra.markets **13 Sep (same day)**, pairex.market 12 Sep, ponsum.app 12 Sep, railx.dev 9 Sep, astralborn.com 7 Sep, prm.market 5 Sep. Every single one is 0 to 8 days old.
- So "took a long time to build" is NOT measurable from the domain, the cert or the first snapshot. It has to be measured two other ways:
  - **surface area now** (routes, auth, docs, API spec, own backend), which is what a long build leaves behind even on a new domain;
  - **whether the TEAM predates the domain**: GitHub history, the founder's X account age and prior products, a prior company site. A serious product ported onto a new domain still has an old author.
- astralborn.com is the one case where the domain had an earlier life (wayback snapshot 11 Jul 2025), which is worth flagging on its own: **a reused domain is a different object from a fresh one.**

# COPY DETECTION, the design that works without a paid index
Keep a local fingerprint corpus, one row per site ever scanned: sorted route-list hash, framework, distinct page shapes, favicon hash, og:image hash, and the set of vendor hosts. **Two projects sharing a route-list hash are the same build.** The corpus grows itself for free because casig already visits every project's site.

# Proposed shape, not yet built
- `techscan` service beside `/opt/casig`, `POST /scan {url, ca, x_handle}`.
- **Tier 1, ~1s, always**: routes + distinct page shapes + catch-all control + home size + framework.
- **Tier 2, ~3s, always, in parallel**: RDAP domain age, docs/openapi fetch, vendor hosts, token contract vs the known pad template bytecode (Pons V2 is exactly 3248 bytes, see [[alert-bot-fleet-7]]).
- **Tier 3, ~6s, only when 1 and 2 pass**: GitHub history, source maps, wayback, corpus copy match.
- Output: one word (REAL BUILD / THIN / SKIN / COPY), then five receipts. Cached forever by domain.

# 13 Sep 2026. Testing the PRODUCT, not the website. Three levels, no gas, no signature.
- [stated] Ben: "Let's say it's a launchpad, a new bonding curve, some API process, a lending protocol... Are you able to test those things and see if they work, if they're actually live, and if they're good?"
- **The mechanism: for a launchpad, a curve or a lending market the CONTRACT IS THE PRODUCT.** The website is marketing about it. So the test is run against the contract, and all three levels are free and read-only.
  1. **`eth_getCode` + hash** -> exact copy detection. Identical bytecode is proof of a fork, not an opinion.
  2. **Selector scan of the runtime bytecode** -> what the contract can DO, with NO verified source needed. Walk the code, and on opcode `0x63` (PUSH4) take the next 4 bytes; skip `0x60..0x7f` push payloads properly or the scan desyncs.
  3. **`eth_call`** -> run the product's own math. A revert on its headline function means the product does not work. State overrides let a swap or a borrow be simulated from a funded address without sending anything.
- **The hard limit, and it is deliberate: no transaction is ever signed or sent.** Anything that needs a real state change (an actual buy, an actual deposit) is simulated, never executed.

## MEASURED, 7 contracts in parallel, 9.1s wall (the public RH RPC is the bottleneck at 6 to 9s per call, not the method)
| what | bytes | selectors | reads |
|---|---|---|---
| pons curve `0x325babf16f57897db229335547fe3c9c438dd44a` | 10,229 | 76 | no owner() |
| pons factory `0x7ed598bcef8bd9edd8c97a195c6d13f40801ec7e` | 24,177 | **143** | owner `0x263ed295dafae1d9aadd6e56c4b6f9f38ee019dd` |
| pons launcher `0xe33e9e479df8802cb0866d5d05258bec4cf62948` | 4,416 | 25 | owner `0x263ed295dafae1d9aadd6e56c4b6f9f38ee019dd` |
| v4 PoolManager `0x8366a39cc670b4001a1121b8f6a443a643e40951` | 24,009 | 39 | owner `0x2bad8182c09f50c8318d769245bea52c32be46cd` |
| PAIREX `0xB8Cf4ad387cfd607C66a207CFFFC46498aACd9d6` | **3,248** | **25** | supply 1e27 |
| RAILX real `0xfEAd46F7baBd84b7f450aaEEcE081deF6e973c28` | **3,248** | **25** | supply 937,562,038.6 |
| RAILX fake `0x8680a02bb54Fe135279b16842D8E90a9ba81e0f0` | 3,854 | **11** | supply 1e27 |

## Two findings worth keeping
- **Byte size plus selector count is a build-effort meter for the contract side, and it needs no source.** 25 selectors is a plain token. 143 is a protocol. A project whose only contract is a 3,248-byte 25-selector pad token has written zero contract code, whatever its website says.
- **The RAILX impostor is independently caught here: 3,854 bytes and only 11 selectors, so it is NOT a Pons launch at all**, while the real one is the exact 3,248 / 25 pad template. This agrees with [[casig-gmgn-overlay]]'s website verdict by a completely different route, which is the kind of agreement that counts (two independent methods, not one signal read twice).

## Per product type, what to actually call
- **Launchpad / curve**: quote function by `eth_call` (reverts = broken); simulate a buy with a state override; count tokens created by the factory and how many DISTINCT deployers and how many graduated; read the fee take rate off the contract instead of the docs.
- **Lending**: totalSupplied / totalBorrowed / utilization read live; simulate a borrow; **read which oracle it uses, because a single EOA pushing prices is the rug**; count distinct suppliers from events (3 addresses means a demo, not a market).
- **API product**: call the documented endpoint with a real input AND a garbage input (a stub answers both the same); compare its answer to ground truth such as DexScreener; five calls for latency spread; a real service rate-limits or wants a key.
- **Any claim**: the docs say X, the selector list says whether X exists. Missing selector, missing power.

# 13 Sep 2026. Ben killed the selector-count score, and he was right. The replacement.
- [stated] Ben: "it doesn't make sense to say that this thing can do more things than something else. It can do 25 things, and this other thing can only do 11, so then we give the 11 one a worse score. That's not the correct way." He asked for an overlay showing **really strong / okay / weak / scam** that works across every product type.
- **Why counting is wrong, stated from the mechanism: a feature count measures SURFACE, and surface is what a copier gets for free.** Uniswap v2 core is tiny and excellent; a forked bloated contract is large and worthless. Anything a scammer can inflate in an afternoon cannot be the scoring variable.
- **The correct primitive: DID SOMEONE WHO IS NOT THE TEAM SPEND SOMETHING IRREVERSIBLE ON THIS.** Money, time, code, reputation. That is the only thing a copier cannot fake, and it is defined identically for a launchpad, a curve, a lending market, an API and a meme.

## THE FOUR GATES, and they are NOT summed
A sum hides which gate failed, and each failure implies a different action. So: three binaries plus one veto, resolved as a decision tree.
1. **REAL** (pass/fail/unknown). The product's own headline action executes. Simulate it, never count features: quote a buy on the curve, simulate a deposit on the lending market, call the API with good input AND garbage input (a stub answers both the same). A revert is a fail.
2. **THEIRS** (pass/fail/unknown). Bytecode hash equal to another deployment = copy. Route fingerprint equal to another site = copy. Contract is the pad's stock template (3,248 bytes / 25 selectors on Pons) = no code written, which is not a scam but is not a moat either.
3. **USED BY STRANGERS** (pass/fail/unknown). Distinct non-team addresses that performed the costly action: deployers who launched on the pad, suppliers in the market, buyers who were not bundled. **Threshold is a PERCENTILE against the chain's own same-age cohort, never an absolute**, because on this chain every candidate is hours old.
4. **CAN THEY TAKE YOUR MONEY** (veto, overrides everything). Mint / pause / blacklist / tax setter with a live owner, upgradeable proxy, LP unlocked, oracle fed by an EOA, dev already sold, launch bundle, honeypot on a simulated sell, or the casig red (the project's own channel publishes a different contract).

## THE LABELS, and the action each implies
| label | rule | action |
|---|---|---|
| SCAM (red) | any veto fires | avoid |
| STRONG (green) | REAL + THEIRS + USED, no veto | buy |
| OKAY (amber) | REAL, and exactly one of THEIRS or USED | watch |
| WEAK (grey) | REAL but neither, or REAL unknown | skip |
- **UNKNOWN is a first-class state and must be shown, never rounded into a score.** At minute five nothing has usage, so a fresh token is honestly WEAK-unknown, not a fake grade. The label is allowed to IMPROVE as the token ages; cache short on weak, long on scam.
- The click-through shows **four lines, one per gate, each with its receipt**. Never a number. "Real, original, nobody using it yet" and "real, copy, plenty of users" are both OKAY but they are different trades, and the four lines say which.

## THE MEME FORK, which is what makes it work across every product type
Most cards have no product at all, and grading a meme on its tech is Ben's own DD rule 0a violated by machine. **Auto-detect: if there is no site with more than one page shape, no API, and the contract is the stock pad template, the lens is MEME.** Same four labels, same four gates, different contents:
1. REAL -> the reference exists and is used correctly (the name resolves to a real thing).
2. THEIRS -> nobody else already owns this reference on this chain (ticker twin check).
3. USED -> holders arriving, not just price moving.
4. VETO -> unchanged, it is the same contract battery.
- So one overlay, one set of four colours, two rubrics chosen automatically. Nothing else changes.

# 13 Sep 2026. First live run of the four gates: ORIONIS (orionisderivative.tech)
- [stated] Ben pasted the site and asked for a speed test. **Whole thing, three passes, about 25 seconds of probing.** Routes 0.5s, full probe 3.5s, bundle + chain + X + DexScreener 10.9s, API battery 1.8s.
- Token **0xFcEaE4dF6961cBB3d22491b72e66c7Adf415BCA8** (ORIONIS, Pons, Robinhood Chain). Pair created 13 Sep 15:26 UTC. At screen: mc $146,143, liq $35,303, vol24 $476,706, 2,444 buys / 2,026 sells. X **@OrionisDeriv** created 12 Sep 2026, 112 followers, timeline not indexed (the usual twitterapi.io newborn gap).
- **IMPOSTOR TWIN ALREADY LIVE: 0xc00B9f0a8895352Fb1b43a9cBDC69B334eFC007c**, same ticker ORIONIS, pair created 15:34 UTC, **8 minutes after the real one**, mc $6,267, liq $6,267, no website and no socials. Second receipt for the impostor rule, after RAILX.

## Gate results
1. **REAL: front end yes, product no.** 5 distinct page shapes (`/trade` `/perpetuals` `/options` `/markets` `/portfolio`), 1,072,205 bytes of own JS across 15 chunks, server-rendered text (9,108 chars on /trade), honest 404 on an unknown path (no catch-all trap), and `/api/auth/nonce` returns a fresh random nonce, so SIWE auth is genuinely wired.
   - **BUT `/api/market/quotes` IGNORES ITS INPUT.** `?symbol=AAPL`, `?symbol=TSLA`, `?symbol=NVDA` and `?symbol=ZZZZZZNOTREAL` all return the identical 2,253-byte payload headed by NVDA. **The garbage-input control is what caught it**, exactly the design in this file. It is a static list, not a quote engine.
   - **No derivatives contracts exist on Robinhood Chain.** Of 30 candidate addresses scraped from the site and all 15 chunks, only three have code: the ORIONIS token (3,248 bytes, the stock Pons template), multicall3 `0xca11bde05977b3631167028862be2a173976ca11`, and the ENS registry address `0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e`. Both of the latter are library constants, not theirs.
2. **THEIRS: the app yes, the protocol does not exist.** The token is the stock pad template, so zero contract code was written.
3. **USED BY STRANGERS: the token is traded heavily, the product is used by nobody**, because there is nothing on chain to use.
4. **VETO: none fired.** Stock Pons token means no mint, pause, blacklist or tax. The X bio publishes the correct CA, so casig reads green.
- **Verdict: OKAY, leaning weak. Not a scam, but the advertised options-and-perps stack is not live.** What would change it: real contracts deployed on Robinhood Chain, and a quotes endpoint that answers differently for different symbols.

## Two reusable lessons from this run
- **A bundle's host list is mostly library noise.** The chunk scan returned etherscan, polygonscan, bscscan, arbiscan, thirdweb and a dozen blockscouts: that is the default wagmi/viem chain config, NOT evidence of rented upstreams. **Read vendor hosts from the HTML, not from the bundle.**
- **Same for addresses.** `0x2f684bda12f684bda12f684bda12f684bda12f68`, `0x4bda12f6...`, `0x4e487b71...` (the Solidity Panic selector padded) are library constants. **Filter repeating-nibble patterns and then require code on the chain in question**, which is what reduced 30 candidates to 1 real contract.

# 13 Sep 2026. ORIONIS re-check. I WAS WRONG ABOUT THE PRICE FEED, and the real finding is much worse.

## CORRECTION: the quotes endpoint is REAL. Retract the "fake feed" call.
- I said `/api/market/quotes` was a stub because `?symbol=AAPL`, `?symbol=TSLA` and `?symbol=GARBAGE` all returned the identical payload. **That test was invalid: the endpoint takes no parameter at all.** The bundle shows the app calling `fetch("/api/market/quotes",{cache:"no-store"})` with no query, and it returns a fixed 10-symbol board (NVDA TSLA AAPL META HOOD MSFT AMZN GOOGL SPY QQQ).
- **The prices are correct to the cent** against Friday 11 Sep 2026 closes: NVDA 218.29, AAPL 332.27, HOOD 112.57, all three matched exactly. The feed is live and real.
- **THE RULE THIS PRODUCES, and it is the mechanical fix: the garbage-input control only works on an endpoint that TAKES input. For an unparameterised feed the only valid test is comparing its VALUES to ground truth.** Before calling any data fake, first read how the front end calls it, then pick the matching test. I skipped that and got it backwards.
- Second error in the same pass: I used the tokenized equities on Robinhood Chain as a price reference. **They are not one.** Against the real closes: SPY on chain is 0.5% off, but NVDA is 35% off, AAPL 39% off, TSLA 30% off. **Never price a real-world asset off a thin RH pool; only SPY is deep enough to track.**

## THE ACTUAL FINDING: twelve contracts published as "MAINNET ONLINE", all twelve are EMPTY ADDRESSES
The site ships its own contract registry in the bundle, each with a name, a .sol filename, an address and the status string "MAINNET ONLINE". Every one returns zero bytes from `eth_getCode` on Robinhood Chain.

| contract | address | code on chain |
|---|---|---|
| MarketRegistry.sol | 0x939A91DeF32B07B05E13a01418346C918229E905 | 0 bytes |
| OrionisVault.sol | 0x7fB05287fA56271A63c367264D623D619313BC3A | 0 bytes |
| FeeManager.sol | 0x592d65Ae3B919AcaDa668CD8a62eA3086a7A5509 | 0 bytes |
| OracleRouter.sol | 0x424117F70a14bb3B2B0c2a0db91435d69dd47e4C | 0 bytes |
| MarginEngine.sol | 0x8AF58B76BBD6Dff84251F5dC2005f09d4dFf757B | 0 bytes |
| RiskManager.sol | 0x91E05Bc5F07330814A075d190Cf2e07c86D8b480 | 0 bytes |
| OptionsEngine.sol | 0x4D4288f7148868a779776C6671dfBe0CeAcFdDfE | 0 bytes |
| OptionSettlement.sol | 0xbd436Fb65a0560F0f6e9A46a40fe003c7A998450 | 0 bytes |
| PerpsEngine.sol | 0x9D5701BCFd9DEd0447E5dF39ba0cb37995559De1 | 0 bytes |
| FundingManager.sol | 0x10C6fD332DB009F34c13F29ED8FFe9B937DaE5B3 | 0 bytes |
| LiquidationEngine.sol | 0x796eF231F032fA1A46b75750797cC0922c985F46 | 0 bytes |
| USDG.sol "CollateralToken" | 0xF47593cac046C3a4C15B495eDAd59DE5868B6BbB | 0 bytes |
- **The USDG row is the tell that it is fabricated rather than merely unshipped:** real USDG on Robinhood Chain is `0x5fc5360d0400a0fd4f2af552add042d716f1d168` (170 bytes). They invented an address for a token that already exists and that they did not deploy.
- Controls run in the same batch on the same RPC: ORIONIS token 3,248 bytes, real USDG 170 bytes. So the RPC was answering; the zeroes are real.
- **Verdict moves from "okay, product not live yet" to a false claim.** The website is good, the price feed is real, and the entire protocol it advertises is a list of addresses with nothing at them.
- **NEW STANDING CHECK, cheap and devastating: scrape the project's OWN published contract list out of its bundle and `eth_getCode` every address.** Pattern that found it: `name:\"..\",symbol:\"...sol\",address:\"0x..\",status:\"..\"`. Generalise to any addresses-with-labels block. A project that names its contracts is handing over the test.

## PINNED TWEET: fixed and deployed in casig
- [stated] Ben: "I've seen you put a question mark when you're checking the contract address in the pinned tweet. You just have to look at the pinned tweet... That cannot happen."
- **`twitter/user/info` returns `pinnedTweetIds`, and `twitter/tweets?tweet_ids=` reads any tweet by id.** So the pin is readable even on an account minutes old whose `from:` search still returns zero. @OrionisDeriv pin: "Orionis Derivatives is live on mainnet. CA : 0xfceae4df6961cbb3d22491b72e66c7adf415bca8".
- `scan_x` now checks, in order: bio, **pinned tweet**, then recent posts. Backup `casig.py.bak-pin-20260913-155509`. Verified live: ORIONIS real reads green in 4.1s, the twin `0xc00b9f0a8895352fb1b43a9cbdc69b334efc007c` reads **red** naming the real contract.
- Also noted: `from:OrionisDeriv` search returned 0 tweets earlier today and 20 tweets a few hours later. **The twitterapi.io newborn gap closes within hours, so a zero is a timing fact, never a fact about the account.**

## THE SCORE, REBUILT for an hour-one buyer
- [stated] Ben: "I'm assuming nobody will use something when I buy it, so that differentiation doesn't really matter. I'm buying in the first hour or two." And: "how long someone needs, how innovative it is, how differentiated it is, and if it has moat would be some pretty important aspects."
- **So the USED-BY-STRANGERS gate is retired. At hour one it is structurally always unknown, and a gate that can never be decided is not a gate.** Replaced by the three things that ARE observable at minute five:
  1. **WORKS** (unchanged): the headline action executes, and every published contract has code. This is where ORIONIS dies.
  2. **HARD TO COPY**: rebuild estimate in days, driven by the scarce ingredient, not by feature count. A Next.js front end over public APIs is 1 to 2 days. Deployed and working contracts, a data source nobody else has, or a licence is weeks.
  3. **ONLY ONE**: first or only of its kind on this chain, versus the fourth clone of something that already exists here.
- Labels: **STRONG** = all three. **OKAY** = works, plus exactly one of hard-to-copy / only-one. **WEAK** = works but easy to copy and not unique. **SCAM** = the veto (owner powers, dev sold, bundle, wrong CA, or a published contract that does not exist).
- **Say the gate names in the output, never "one of the other two".** Ben flagged that phrase as meaningless, and he was right: the label must carry which gate passed.

# 13 Sep 2026. THE HARDENING RUN: 52 projects, 4 iterations. This is where the process was actually forged.
- [stated] Ben: "make as many mistakes as quickly as possible now so that we improve the process and harden it, so that when we go live, we have made a lot of mistakes free, without putting money on the line."
- Corpus: the live GMGN "almost bonded" and "migrated" columns, topped up from DexScreener to **52 Robinhood Chain projects that publish a website**, plus ORIONIS (known bad), RAILX and PAIREX (known good) as controls. Harness `/opt/casig/batch.py` -> `run1..run4.json`, corpus builder `/opt/casig/build_corpus.py`, corpus `/opt/casig/sites.json`.

## What each iteration found and fixed
**Run 1 (51/52 ok).** Harness bugs first, and they were the dangerous kind:
- One hung site froze the whole batch at 50/52 for four minutes. **No per-site deadline.**
- Results were written only at the end, so the hang would have lost all 50 finished rows. **Now written after every completion.**
- `pkill -f batch.py` over ssh kills the ssh session itself, because the remote command line contains the pattern. Use `pgrep -f` then `kill <pid>`.
- **The headline finding: the contract-registry check fired on 1 site in 51.** The check that destroyed ORIONIS is almost never applicable, so the tool returned "BLOCKED, cannot say" for 50 of 51 and told Ben nothing.

**Run 2 (42/52 ok).** Generalised the registry check and surfaced silent failures:
- **Every 0x address the project PRINTS is a claim, registry or not.** That lifted coverage from 1 site to **20 of 42**.
- A 32-byte response had been recorded as a successful scan. **An empty fetch is a failure, never evidence.** Nine sites were exposed by that guard.
- Template-literal paths (`/api/markets/${e}/pools`) were being requested literally, which produced most of the bogus "ignores its input" hits. Dropped.
- Loose-address lookups were serial against a 6 to 9 second RPC, so one site cost 102 seconds.

**Run 3 (47/52 ok).** The single most valuable finding of the whole exercise:
- **NINE of 52 project websites are DEAD.** They answer with a hosting-platform error: railway `{\"status\":\"error\",\"code\":404,\"message\":\"Application not found\"}`, vercel `DEPLOYMENT_NOT_FOUND`, or a 402 `DEPLOYMENT_DISABLED` payment wall. Run 2 had labelled these "scan failed", **the exact opposite of the truth**. A dead website is a decisive verdict, not a missing datapoint.
- **Page count is the wrong depth proxy for a single-page app.** chinaperps.online renders 4,664 characters of real product text with ZERO hrefs; astralborn.com renders 4,322 characters with 83 buttons and zero hrefs. Both read "0 pages". Replaced with **text characters + distinct headings + buttons + routes**, all four reported.
- **That also corrected an earlier call of mine: astralborn.com is NOT an empty poster.** 83 buttons and 4.3k of text is an interactive app. The 0-routes reading had misled me.
- `0x6e61746976650000000000000000000000000000` is the ASCII word "native" padded to 20 bytes. **An address that decodes to printable ASCII is a string constant, not a contract.**

**Run 4 (49/52 ok, 0 unresolved).** Chain-awareness and one liveness path:
- **11 of the 39 live sites are SOLANA apps** (`/api/raydium/*`, `/api/token/spl-balances`). Checking their printed addresses against the Robinhood RPC and calling them "empty" is nonsense. **Detect which chain the site targets before judging any address.** Only 21 of 39 are judgeable on Robinhood Chain.
- Liveness and the chunk fetcher had different error handling, so four dead sites raised in one path and returned readable error bodies in the other. **One path now, with a curl fallback**, and dead detection went from 6 to **10 of 52**.
- Per-site work budget added. Three huge-bundle sites (launchpad.meme, commodites.market, aquity.finance) still exceed it and are reported as "took too long, re-run alone", which is the honest output.

## THE CORPUS RESULT, and it is the base rate Ben should carry
| what | count of 52 |
|---|---|
| **website DEAD** (coin trades, site is gone or unpaid) | **10 (19%)** |
| empty shell, under 300 characters of rendered text | 6 |
| real site, Solana app (not this chain) | 11 |
| real site, Robinhood Chain, addresses judgeable | 21 |
| publishes a contract registry | 1 (ORIONIS) |
| **publishes an address that does not exist on chain** | 3 (ORIONIS 7 of 8, CHINAPERPS 1 of 1, OVC 1 of 3) |
| too slow to finish inside the budget | 3 |
- Dead sites found: forgemarket.app, stonkslaunch.xyz, launchonhf.com, migratemrkt.com, nomomarkets.buzz, collapick.tech, nvdacat.vip, trade.cv, vaultrobinhood.com, app.stockyield.money.
- **So the cheapest check in the whole stack, "does the website load at all", disqualifies one project in five.** Run it first, always.

## RESIDUAL, known and unfixed
- One site reported 1,134,779 characters of "text": the extractor is counting inlined JSON, so **text_chars is not yet trustworthy above ~50k**. Strip `<script>`/`<style>` fully before measuring.
- A project's listed website is sometimes **somebody else's site** (a robinhood.com newsroom page, a popicon.life article, a github.com file). Those score as deep, well-built sites because they are. **Confirm the domain is the project's own before crediting any depth.**
- The three slow sites need a solo re-run path.

# 13 Sep 2026. SECOND HARDENING RUN: 10 fresh projects, 3 iterations. Corpus `/opt/casig/sites10.json`, runs A1 to A3.
- 5 "new" (7 to 75 hours old, still thin) and 5 "migrated", none overlapping the first 52. Built by `/opt/casig/build10.py`, which reproduces the GMGN new/migrated split from pairCreatedAt and liquidity, so it needs no browser. **Ben's Chrome was in use, and fighting him for the tab is never the right move; the chain gives the same cohort.**
- Whole run: **10 sites in 25 to 34 seconds.** No dead sites in this cohort (the first corpus was older and broader), no false contract claims.

## A1 -> A2: two real bugs, both false accusations
- **CONTRACT BYTECODE WAS PARSING AS AN ADDRESS.** divilend.space prints deploy bytecode on the page, and `0x[0-9a-fA-F]{40}` matched the first 20 bytes of it: `0x608060405234801561001057600080fd5b506040` is the standard EVM prologue, not a contract. **That produced 3 false "this address does not exist" accusations on one site.** Fixed with a bounded regex, `(?<![0-9a-fA-FxX])0x[0-9a-fA-F]{40}(?![0-9a-fA-F])`, plus an explicit bytecode-prologue reject. DIVILEND went 3 false -> 0.
- **A catch-all site still scored "live" endpoints.** plip.run renders **10 characters of text**, answers 200 to any path, and was credited with 3 live APIs. Now a catch-all discards all endpoint evidence for that site: Plip 3 -> 0, largedata.center 2 -> 0.
- **RULE: any measure taken from a site that answers 200 to anything is void, not merely weaker.**

## A2: a new signal worth keeping, CHAIN MISMATCH
- **datacenterwatch.ai and 0swap.exchange are substantial sites that mention SOLANA and never mention this chain**, while their token trades on Robinhood Chain. That is a site built for something else, wearing this token's name. `chain_mismatch()` now reports "SITE TARGETS SOLANA, NOT ROBINHOOD".

## A3: the conceptual gap, and it is the biggest lesson of this batch
- **The scanner was grading the WEBSITE and never asking whether the website is about THIS TOKEN.**
- **5 of 10 sites never print their own contract address anywhere**, page or bundle: OPTIONSPAD, SPRL, DIVILEND, Plip, VOXEL. **voxelithic.xyz has the deepest site in the batch (65,370 characters, 7 routes) and never names its own token.** A beautiful site for a different coin is worth nothing to the buyer of this one.
- `own_ca_on_site` / `own_ca_where` (page / bundle / nowhere) is now a first-class output of every scan.

## The batch result, and the one clear reject
| sym | kind | text | routes | own CA | flags |
|---|---|---|---|---|---|
| DATACENTER | migrated | 51,548 | 28 | page | targets Solana |
| LDC | migrated | 39,564 | 3 | page | answers 200 to anything |
| VOXEL | migrated | 65,370 | 7 | **nowhere** | |
| SPRL | migrated | 5,560 | 5 | **nowhere** | |
| ZERO | migrated | 1,981 | 1 | page | targets Solana |
| EQUITY | new | 3,471 | 7 | bundle | |
| OPTIONSPAD | new | 3,688 | 4 | **nowhere** | |
| BRAIN | new | 1,526 | 0 | page | no chain named |
| DIVILEND | new | 1,518 | 7 | **nowhere** | |
| **Plip** | new | **10** | 0 | **nowhere** | prints a contract that does not exist; answers 200 to anything; empty shell |
- **Plip (plip.run, token `0xaba8866fec42d193373e251d3ed200320d220503`) is the reject.** Ten characters of rendered text, a catch-all, and it prints `0x13bbfb7f4c5bbb1f0e5a63570beef34a909d646a` which has zero code on chain while its own token sits at a different address it never mentions.

## Gate 9, added to the pre-delivery gate at the top of this file
**Does the site name the token you would be buying?** page / bundle / nowhere. "Nowhere" is not fatal on its own (half this batch), but it downgrades every other thing the site scores, because none of that work is provably about this coin.

# 13 Sep 2026. VERSION 2 RUN: 11 fresh projects, new + almost bonded + migrated, 3 passes (then 4 confirm passes).
- [stated] Ben named the hardened scanner **version 2** and asked for 10 new projects off the same GMGN page covering **new, almost bonded AND migrated**, 3 passes, with accuracy of what is reflected and scored, plus speed, as the goal.
- Corpus `/opt/casig/sites10b.json`, runs B1 to B7. **Built from the live GMGN DOM, not from DexScreener**, because the pre-migration "new" and "almost bonded" tokens have no DexScreener pair at all and are invisible to `build10.py`. The three columns are `div[class~=/flex flex-col flex-1 border-line-100/]`, index 0 new, 1 almost bonded, 2 migrated; the project website and X handle are on the card as external hrefs beside the ponsfamily launchpad link.
- **11 sites in 29 to 31 seconds, every pass, zero errors, zero timeouts.** Speed is solved; every remaining fix this run was about accuracy.

## The five defects found and fixed, in order
- **P1 text_chars was measuring markup.** `<style>`, inline `<svg>` path data, `<noscript>`, `<template>`, HTML comments and base64 blobs all counted as "text". New `visible_text()` strips all of them and drops any token of 45+ characters with no space; `depth()` now also reports **words**, which is the honest number.
- **P2 the listed website is often not the project's own page.** New `own_site()` returns own/not-own with the reason. Two independent failure modes, and they mean different things:
  - **shared host** (forms.gle, github.com, docs.google.com, medium.com, notion.site, robinhood.com, ponsfamily.com, app.long.xyz, popicon.life, x.com, t.me). ORDER's "website" is a Google Form. Nothing about that page is evidence about the token.
  - **own domain, but the page never names the token**: no contract, no ticker, no X handle. drivekey.info is a real, substantial product page that never once says DVK.
- **P3 P4 a client-rendered app has NO text in its homepage HTML.** v2.1 scored HARVEST at 7 words, ALPHA at 6, HYIP at 1, TALON at 7, which is the same reading an empty poster gives. **Headless rendering is not available: the droplet has 961MB RAM and 17 live bots, and the Cowork container's egress allowlist blocks these domains outright, so Playwright there returns ERR_CONNECTION_RESET on every one.** The working substitute is `bundle_copy()`: pull quoted string literals of 12+ characters containing a space, at least 75% letters, deduplicated. HARVEST 7 html words -> 1,113 bundle words. ALPHA 6 -> 3,497. HYIP 1 -> 3,218. TALON 7 -> 3,849. Only the Google Form scored nothing.
- **P6 vendor library strings inflated that count.** React, Next, axios, KaTeX, wagmi and viem all ship long English error messages. DASP's entire "product copy" was KaTeX. A VENDOR stoplist now drops them.

## THE FINDING OF THIS RUN: read the copy the project wrote for itself, and it will tell you it is not real
Three sentences, all pulled straight out of the bundles, none visible on the rendered page:
- stonkharvest.com: **"You're reading the standalone rebuild's field notes. This build runs locally with simulated currency, Stock Token allotments and a practice Genesis vessel."**
- talonfinance.app: **"This network uses mock tokens with no market value and a funded mock exchange. Real deposits are not enabled."**
- agentalpha.pro: "Live mode trades Robinhood Chain stock & ETF tokens. Crypto and options apply to demo only."
`simulation()` now scans for simulated/mock/no market value/testnet only/demo mode/paper trading/placeholder data/not real funds and returns each hit with 90 characters of context either side.

## P7 P9: PRECISION, because a false "it is fake" costs a trade
First version fired on 5 of 11 and **two were wrong**. agentalpha.pro has a Live mode AND a Demo mode, so its demo copy is a feature; hyip.fi scored on a "Coming soon" attached to a social-link placeholder. `sim_tier()` now returns three tiers:
- **BLANKET** the build says the whole thing is not real. Decisive. Signature: the sentence contains "this build", "this network", "this app", "real deposits are not".
- **MODE** a demo or mock path that sits beside a live one. Signature: a ternary in the context, a `mode`/`checked:`/`label:` key, or any demo/virtual/practice word while the bundle also contains a live-mode branch.
- **NOISE** a placeholder label ("coming soon", "not yet live"). Dropped.
After tiering: 2 BLANKET (HARVEST, TALON), 2 MODE (MOONTAGE, ALPHA), 0 noise. Both BLANKET calls were read by hand and are correct.

## P8: THE DEFAULT BEATS THE COPY. Read the app's boot state.
Copy can be argued with; a default cannot. talonfinance.app ships `useState({mode:"preview", chainId:46630, rpcUrl:"https://rpc.testnet.chain.robinhood.com"})`. The mainnet branch (`chainId 4663`, USDG, Uniswap V3 adapter) exists in the code and **is not the one the app boots into**; the boot branch is testUSDG on a mock exchange. `boot_config()` now reports the rpc urls, the chain ids, which are testnets, and `no_chain_code` when there are none at all.
- **P9 tightening: a bare `chainId:5` inside a bundled chain registry is not a default.** hyip.fi matched 5 alongside nine other ids and boots on no chain. Testnet only fires on an explicit testnet RPC, or a lone testnet id with nothing else.
- **7 of 11 sites have no wallet or chain code whatsoever**: Hold, HARVEST, MOONTAGE, FOMOSSAUR, DVK, ORDER, DASP. A site with no rpc url and no chainId cannot do anything on chain, whatever it says.

## The B corpus result
| sym | column | contract | html words | bundle words | flags |
|---|---|---|---|---|---|
| COLT `0xf61836823abab0152842df9a53fb8dfee354e094` | new | on page | 556 | 2,096 | 5 routes, real app copy |
| Hold `0xf40bcd868c1c3d91023ced508d16d0611e2ef371` | almost bonded | on page | 840 | 57 | no chain code |
| ALPHA `0x767dc0f754889f3921cac0958502c1b92204d390` | new | nowhere | 6 | 3,497 | live mode + demo mode |
| HYIP `0x32c7de47c4fdd037a90394ccc34e1c2b47f58cb4` | new | nowhere | 1 | 3,218 | deep copy, no chain code |
| TALON `0xa4f289ebd33465b0657a45ce1693a49f46707a2c` | migrated | in bundle | 7 | 3,849 | **boots on testnet, mock tokens** |
| HARVEST `0x8acbe44f8e0fcc01da60114791d48e254502e741` | new | nowhere | 7 | 1,113 | **simulated currency, whole build** |
| MOONTAGE `0x200e1afc6aac7ad2fdb50f352f3bf313888540bd` | almost bonded | nowhere | 558 | 750 | demo-mode pill |
| DASP `0x1c2c820c3053411ab32239fb1644a315942901fe` | migrated | nowhere | 1,089 | 236 (all KaTeX) | no chain code |
| FOMOSSAUR `0x3d4d5a135c8a4db93493f63e5085e0f1e6d63344` | migrated | nowhere | 627 | 47 | no chain code |
| DVK `0x583b10d1912e8d3def8709a6c1cb7ac88fa27fd7` | almost bonded | nowhere | 620 | 1,044 | **page never names the token** |
| ORDER `0x8cbd6facc468012ec55ec5801f578aa75280cc13` | migrated | nowhere | 0 | 18 | **"website" is a Google Form** |
- **8 of 11 never name their own contract anywhere.** Same rate as the A batch. This is the normal state of a Pons launch, not an exception.
- COLT and Hold are the only two that both name their own contract and carry real product copy.

## Gates added to the pre-delivery gate
11. **Is this page the project's own?** Shared host, or own domain that never names the token, both void the depth score.
12. **A client-rendered app is measured from its bundle copy, never from its homepage HTML.** Zero rendered words is not evidence of an empty product.
13. **Search the bundle for the project's own admission.** simulated / mock / no market value / real deposits not enabled. Tier it BLANKET vs MODE before reporting, because a demo mode beside a live mode is a feature.
14. **Read the app's boot config, not only its prose.** Default chainId and rpcUrl beat any sentence on the page.

# 13 Sep 2026. V4: the trench-scanner port kit, applied. Gate at `/opt/casig/selftest.py`, deploy at `/opt/casig/deploy.sh`.
- [stated] Ben named the previous scanner **V3** and handed over a port kit from the rugscan rebuild, asking for only what makes this more precise, less error-prone and more efficient.

## PORTED, with what each one actually caught here
- **L3 fail closed, and it was a live false-accusation path.** `code_size` did `.get("result") or "0x"` -> **0 bytes -> "this address does not exist"**. An RPC `error` object with HTTP 200 therefore read as an empty contract. Now an error or a missing `result` is None -> -1 -> UNKNOWN, and zero only ever comes from a real `0x`.
- **SPEED, measured, not assumed. The Robinhood RPC accepts JSON-RPC batches.** Droplet: one `eth_getCode` **1.19s**, fifty in one batch **1.97s**. 50 is the ceiling, 100 returns 429. Plus a permanent on-disk cache at `/opt/casig/codecache.json` (positives forever, zeros for 10 minutes). Repeat lookups are now 0.000s. **This retires the "the public RPC is 6 to 9 seconds per call" line in this file: that was serial cost, not call cost.**
- **L6 a rate limit is not a dead site.** The old curl fallback marked any non-200 with a short body as dead. 408/425/429/500/502/503/504/52x are now `unknown=True` and produce a new verdict **COULD NOT REACH**, never SITE DEAD. Given SITE DEAD fires on one project in five, a false one is expensive. 429 on a batch also now backs off and retries the SAME batch before ever splitting it.
- **L4 a detector that silently measures nothing.** `fetch_everything` did a bare `except: pass` per chunk, appended `get()`'s ERROR STRING into the bundle, and returned `len(urls)` as though everything arrived. A half-fetched bundle and a complete one were indistinguishable. Now it returns `(home, blob, found, got)`, only 2xx bodies over 40 bytes are appended, and when `got < found` every bundle-derived NEGATIVE becomes `unknown (bundle partial)`. Fired immediately on Hold and FOMOSSAUR (0 of 1 chunks).
- **L7 resolve one indirection hop.** Two places: a homepage under 4,000 bytes whose only content is a meta-refresh or `location.href` is followed once and the destination is scanned instead; and EIP-1167 minimal proxies (`0x363d3d373d3d3d363d73<impl>5af43d82803e903d91602b57fd5bf3`, 45 bytes) are parsed so a proxy token is never read as "tiny contract, nothing there". Blind must not report as clean.
- **L9 patching is not deploying.** `deploy.sh` does four things in order: parse, import, run the gate, `systemctl restart casig.service`. The overlay daemon held the old modules in memory, so every previous fix shipped to disk and not to the running service.
- **Section 4, THE GATE.** `selftest.py` replays every mistake this scanner has made, against live data: ORIONIS still finds its false registry, PAIREX still raises no false alarm, DIVILEND's deploy bytecode is still not an address (and a real printed address is still kept), PLIP is still a catch-all, ASTRALBORN is still not an empty poster, HARVEST is still BLANKET, ALPHA is still NOT called fake, HYIP still gets no testnet flag, TALON still boots on testnet, forms.gle is still not a project page, plus unit cases for L3, L6, L1 and sim tiering. **33 checks, 11 seconds, all green.** Cases are added, never deleted.
- **L1 n travels with the statistic**: `bundle_copy` already returns `strings` alongside `words`, and the gate asserts it.

## THE GATE EARNED ITS KEEP ON ITS FIRST RUN
It failed the "bytecode prologue is not an address" case, and the failure was correct: the protection lives in `loose_addresses` (via `BYTECODE_HEAD`), **not** in the `ADDR` regex, which still matches a bare prologue string. Anyone reusing `ADDR` directly would reintroduce the false accusation. The case now tests the real invariant.

## NOT PORTED, and why
- **L2 net-not-gross passthrough, and detectors A to E** (round-trip cost, escape rate, opening-cohort overhang, young-token gate, fresh wallets) are transfer-ledger work. They belong in [[rugscan-4]] and `seller_ledger.py`, not in a website scanner. L2's `pre/postTokenBalances` advice is Solana-specific and this chain is EVM.
- **L8 never score small** is already a standing permanent rule and this scanner has never scored pool depth.
- **The ladder** has no analogue: there is no time window to widen on a website scan.
- **Detector F ticker collision** is already live in casig's `real_tokens()` with a DexScreener volume floor.
- **Token-2022 extensions** are Solana and explicitly unverified in the kit.

## A FINDING THE PORT PRODUCED BY ACCIDENT, and it changes how negatives are reported
drivekey.info read **"the page never names this token"** at 17:05 UTC and **"prints its own contract on the page"** at 17:39. No redirect hop was involved and the scanner did not change that reading: **the site did.** A project at minute 40 is not the project at minute 100. So every negative this scanner emits is a timestamped observation, never a permanent property, and `scanned_at` is now written on every row. A "nowhere" on a token under two hours old deserves a re-scan before it is used against the project.

## Gates added
15. **An RPC error is never zero bytes.** Unknown gets its own bucket and is scored in neither direction.
16. **A 429, a 503 or a timeout is COULD NOT REACH, never SITE DEAD.**
17. **If the bundle is partial, every negative drawn from it is UNKNOWN.** Report `got/found`.
18. **Nothing ships without `deploy.sh`**: parse, import, gate, restart. The gate blocks the deploy.
