M1 · Project analysis (T10–T16)
Exit gate: the team pastes a testnet token address in the back-office and gets, in under 10 minutes, a complete analysis: on-chain facts, target checks, safety checks, socials and docs, recovery score. Nothing scans the whole chain.
T10 · Pons token reader ⚠️ Pons#
Goal Read everything Vivra needs about ONE Pons token from the chain.
Depends on T02
Read first annexe-contrats.md, analyse-des-projets.md (Ce que le Scanner lit), token-vivra-et-tresorerie.md
Build packages/chain/src/abi/{ponsFactoryV2,ponsEscrow,ponsMemeHook,ponsCurve}.json, packages/chain/src/read/pons.ts, packages/chain/src/read/contractCheck.ts
Steps
- Fetch verified ABIs from the Robinhood Chain explorer for the factory, escrow and hook addresses of annexe D, and for one example curve. Save as JSON. If a contract is not verified: stop and ask.
readTokenInfo(address)→{ ponsVersion, launchedAt, launchBlock, creator, feeRecipient, pendingFeeRecipient, creatorTaxBps, pairAsset, phase: 'curve'|'graduated', poolId?, curveProgressPct? }using factory events/views (TokenLaunched,pendingCreatorFeeRecipient, …). Exact function names come from the ABI; map them in one place.contractCheck(address)→{ hasMint, canSell, hasOwner, ownerFunctions[], liquidityLocked }:hasMint: ABI or bytecode selector scan for mint-like selectors (mint,_mintexposed) on the token.canSell:simulateContractof a small sell on the pool/curve from a funded fork account oreth_callwith state override; failure reasons recorded.ownerFunctions: list of non-view functions guarded by owner (ABI names containing pause, blacklist, setFee, setTax, mint…).liquidityLocked: graduated Pons v2 → true by design (pool owned by hook); curve → true.
quoteBuy(token, amountInWei)→ expected tokens out + price impact %, used by the recovery score (buyback impact).
Done when
- Fixture test:
readTokenInfoon recorded RPC responses returns the expected object for one graduated and one curve token -
TESTNET=1test:readTokenInfoon a real testnet Pons token succeeds -
contractCheckflags a fixture ERC-20 with an exposedmint(address,uint256)ashasMint: true -
quoteBuyreturns a positive impact that grows withamountIn(property test with 3 amounts)
Out of scope History, holders, price series (T11).
T11 · Token history loader#
Goal Build holders, positions, prices and volumes for ONE token from its own logs since launch.
Depends on T01, T02, T10
Read first analyse-des-projets.md, annexe-base-de-donnees.md (token_snapshots, holder_positions), annexe-score-de-reprise.md (prix de revient, holders sous l'eau)
Build packages/chain/src/read/history.ts, packages/core/src/positions.ts, apps/workers/src/jobs/projects.loadHistory.ts
Steps
- Fetch
Transferlogs of the token and swap/trade logs of its curve or pool fromlaunchBlockto head withgetLogsChunked. - In
packages/core/positions.ts(pure): replay events to compute per-wallet balance, first-held time, bought amount and average cost in USD (ETH price per block from a provided price function), excluding pools, burn address and the curve. - Compute and store:
holder_positionsrows; dailytoken_snapshotsfor the last 120 days (price, market cap, liquidity, volume 24h/7d, holders, top10 %, dev bag %); ATH price and ATH market cap;died_at(first day the Dead rules are all true). - Dust threshold, excluded addresses and ETH/USD source are settings (
history.dustRaw,history.excluded,prices.ethUsdSource).
Done when
- Unit test: replaying a 6-event fixture gives exact balances, first-held times and avg costs
- Unit test: pools, burn and curve addresses are never counted as holders
- Integration test on a recorded log fixture of ~10k events: runs in < 30s and writes snapshots
-
TESTNET=1: loading a real testnet token completes andholders > 0
Out of scope Continuous tracking (T41/T31 use this code later).
T12 · Dev wallet tracer#
Goal Find the creator's linked wallets and their share of supply.
Depends on T10, T11
Read first fiche-scanner.md, annexe-agents-prompts-outils.md (chain_trace_funding)
Build packages/chain/src/read/funding.ts, packages/core/src/devBag.ts
Steps
traceFunding(wallet, depth=2): native ETH transfers in/out of the wallet (via explorer APItxlistif available, elseeth_getBlockscanning is NOT allowed — stop and ask). Return edges{from,to,value,block}.- Linked wallets = creator + wallets funded by the creator + wallets that funded the creator, depth 2, excluding known exchanges/bridges (setting
funding.ignore). devBag(positions, linked)→ % of supply held by creator + linked wallets. Storedev_wallets.
Done when
- Unit test on a 7-node fixture graph returns exactly the expected linked set
- An exchange address in
funding.ignoreis never followed -
devBagon fixture positions = expected % to 3 decimals
Out of scope Labelling insiders by behaviour (later).
T13 · Socials and docs checker#
Goal For ONE project: X account status and stats, Telegram size, website/docs/whitepaper/GitHub availability, plus avatar and banner.
Depends on T01
Read first analyse-des-projets.md (cible, données lues), annexe-score-de-reprise.md (pilier 4)
Build packages/core/src/socials.ts (types + rules), apps/workers/src/lib/{xApi.ts,http.ts,github.ts,telegram.ts}, apps/workers/src/jobs/projects.checkSocials.ts
Steps
- Inputs: links provided at submission (
tokens.submitted_links) + links found in token metadata / website. - X (API v2, app token): user lookup by handle → exists, suspended, followers, last tweet date (
/users/:id/tweets?max_results=5), profile image + banner URLs. Count mentions of the ticker from other accounts in 7 days (recent search). Respect rate limits; cache 24h. - HTTP checks with a 10s timeout and a real UA: site, docs (GitBook/Notion/docs.*), whitepaper (PDF) → status
active|dead|unknown. - GitHub API: repo exists, public, commit count (default branch), last commit date.
- Telegram: member count from the public
t.me/<group>page meta when available; activity only if our bot is in the group. - Download avatar and banner and upload to object storage (S3/R2); store URLs in
tokens.avatar_url,tokens.banner_url. - Write
socialsrows (one per type) withmetric,last_activity_at,account_exists,status.
Done when
- Unit tests on recorded API fixtures: deleted X account →
account_exists=false; silent 40 days →last_activity_at40 days ago - A 404 docs URL →
status='dead'; a 200 →active - Avatar is re-hosted (URL points to our bucket, not to X)
- No API key appears in logs (grep test on captured logs)
Out of scope Posting anything (M2/M4).
T14 · Dead, target and safety evaluation#
Goal Pure functions that turn the collected data into dead_checks with reasons, using settings.
Depends on T11, T12, T13
Read first analyse-des-projets.md (Statut Dead, cible, règles de sécurité)
Build packages/core/src/evaluate.ts, packages/core/src/settings.ts (registry with defaults), tests
Steps
evaluateDead(snapshots, devActivity, settings)→{ isDead, volumeOk, drawdownOk, devInactiveOk }(avg volume < $1,000/day over 7d, drawdown ≥ 85% from ATH, no creator tx for 14 days).evaluateTarget(token, snapshots, socials, settings)→ each rule pass/fail: ATH mcap ≥ $500K, current mcap $15K–$50K, X exists, X silent ≥ 30 days, ≥ 1 docs source active,is_techconfirmed (null = "pending review").evaluateSafety(contractCheck, positions, devBag, settings)→ holders > 300, no mint, can sell, liquidity locked, dev bag < 5%; bonuses (v2, creator tax > 0) reported but not blocking.- Combine:
eligible = target_ok && safety_ok;exclusion_reasons= human strings for every failed rule (shown in back-office and on the public dossier if published anyway). - Settings registry: key, type, default, description. Defaults exactly from the spec.
Done when
- One test per rule, pass and fail, at the threshold boundaries (e.g. 15,000 and 14,999.99)
- A token with
is_tech = nullistarget_ok = falsewith reason "tech status pending review" - Changing a setting value changes the result without code change (test)
Out of scope Recovery score (T15).
T15 · Recovery score#
Goal recoveryScore(input, weights) exactly as annexe E, with stored components.
Depends on T14
Read first annexe-score-de-reprise.md (entire page)
Build packages/core/src/recoveryScore.ts, packages/core/src/recoveryScore.test.ts, apps/workers/src/jobs/projects.score.ts
Steps
- Implement the 5 pillars and every criterion with the formulas, caps and log scales of annexe E. Missing data → 0 points +
missing: truein components. - Return
{ score: number (rounded int), pillars: [{ key, points, max, criteria: [{ key, raw, ratio, points, missing }] }], weightsVersion }. - Weights and bounds from settings
recovery.*; bumping any → newweights_version. - Job writes
recovery_scoresafter T11–T14 for the token.
Done when
- The worked example of annexe E returns 68 with the exact per-pillar values (13.6, 15.5, 15.2, 12.1, 11.7) ± 0.1
- Edge cases tested: 0 holders, all data missing (score 0, no NaN), values above every bound (score 100)
- Changing one weight produces a new
weights_versionand does not alter stored frozen scores
Out of scope Display (T42).
T16 · Submit and review in the back-office#
Goal The team submits an address, watches the analysis run, then validates or rejects.
Depends on T03, T04, T10–T15
Read first analyse-des-projets.md (Le parcours), annexe-api.md (Back-office), cycles-candidats-et-vote.md (Back-office des cycles)
Build apps/api/src/routes/admin/{auth,projects}.ts, apps/admin/app/(auth)/login/page.tsx, apps/admin/app/projects/{page.tsx,[address]/page.tsx}, apps/workers/src/jobs/projects.analyze.ts (pipeline)
Steps
- Admin auth: SIWE (
/admin/auth/nonce,/admin/auth/verify), onlyteam_wallets.activeaddresses; cookievivra_admin12h; every write logsadmin_audit. POST /admin/projects { address, links? }→ creates tokensubmitted, enqueuesprojects.analyze(T10 → T11 → T12 → T13 → T14 → T15, each step updating a progress field), status →analyzed.GET /admin/projects?status=list with score, eligibility, reasons.GET /admin/projects/:addressfull analysis.POST /admin/projects/:address/review { decision: validate|reject|rerun, reason? };rejectrequires a reason; validating with a failed blocking rule requires a reason too.- Admin UI: submit form, list with filters, detail page showing every check (green / red / "not found"), score breakdown, socials with links, avatar/banner, and the three action buttons. Progress bar while analysis runs (poll every 3s).
Done when
- Non-whitelisted wallet cannot log in (test)
- Submitting a testnet address shows a full analysis in < 10 min (manual, record the time in the PR)
- Rejecting without a reason → 400; rejected tokens never appear in any public API route (test)
- Every admin write creates an
admin_auditrow (test)
Out of scope Building cycles (T41), Scanner AI write-up (T24).