# 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**
1. 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.
2. `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.
3. `contractCheck(address)` → `{ hasMint, canSell, hasOwner, ownerFunctions[], liquidityLocked }`:
   - `hasMint`: ABI or bytecode selector scan for mint-like selectors (`mint`, `_mint` exposed) on the token.
   - `canSell`: `simulateContract` of a small sell on the pool/curve from a funded fork account or `eth_call` with 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.
4. `quoteBuy(token, amountInWei)` → expected tokens out + price impact %, used by the recovery score (buyback impact).

**Done when**
- [ ] Fixture test: `readTokenInfo` on recorded RPC responses returns the expected object for one graduated and one curve token
- [ ] `TESTNET=1` test: `readTokenInfo` on a real testnet Pons token succeeds
- [ ] `contractCheck` flags a fixture ERC-20 with an exposed `mint(address,uint256)` as `hasMint: true`
- [ ] `quoteBuy` returns a positive impact that grows with `amountIn` (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**
1. Fetch `Transfer` logs of the token and swap/trade logs of its curve or pool from `launchBlock` to head with `getLogsChunked`.
2. 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.
3. Compute and store: `holder_positions` rows; daily `token_snapshots` for 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).
4. 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 and `holders > 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**
1. `traceFunding(wallet, depth=2)`: native ETH transfers in/out of the wallet (via explorer API `txlist` if available, else `eth_getBlock` scanning is NOT allowed — stop and ask). Return edges `{from,to,value,block}`.
2. Linked wallets = creator + wallets funded by the creator + wallets that funded the creator, depth 2, excluding known exchanges/bridges (setting `funding.ignore`).
3. `devBag(positions, linked)` → % of supply held by creator + linked wallets. Store `dev_wallets`.

**Done when**
- [ ] Unit test on a 7-node fixture graph returns exactly the expected linked set
- [ ] An exchange address in `funding.ignore` is never followed
- [ ] `devBag` on 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**
1. Inputs: links provided at submission (`tokens.submitted_links`) + links found in token metadata / website.
2. 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.
3. HTTP checks with a 10s timeout and a real UA: site, docs (GitBook/Notion/docs.*), whitepaper (PDF) → status `active|dead|unknown`.
4. GitHub API: repo exists, public, commit count (default branch), last commit date.
5. Telegram: member count from the public `t.me/<group>` page meta when available; activity only if our bot is in the group.
6. Download avatar and banner and upload to object storage (S3/R2); store URLs in `tokens.avatar_url`, `tokens.banner_url`.
7. Write `socials` rows (one per type) with `metric`, `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_at` 40 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**
1. `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).
2. `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_tech` confirmed (null = "pending review").
3. `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.
4. 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).
5. 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 = null` is `target_ok = false` with 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**
1. Implement the 5 pillars and every criterion with the formulas, caps and log scales of annexe E. Missing data → 0 points + `missing: true` in components.
2. Return `{ score: number (rounded int), pillars: [{ key, points, max, criteria: [{ key, raw, ratio, points, missing }] }], weightsVersion }`.
3. Weights and bounds from settings `recovery.*`; bumping any → new `weights_version`.
4. Job writes `recovery_scores` after 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_version` and 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**
1. Admin auth: SIWE (`/admin/auth/nonce`, `/admin/auth/verify`), only `team_wallets.active` addresses; cookie `vivra_admin` 12h; every write logs `admin_audit`.
2. `POST /admin/projects { address, links? }` → creates token `submitted`, enqueues `projects.analyze` (T10 → T11 → T12 → T13 → T14 → T15, each step updating a progress field), status → `analyzed`.
3. `GET /admin/projects?status=` list with score, eligibility, reasons. `GET /admin/projects/:address` full analysis.
4. `POST /admin/projects/:address/review { decision: validate|reject|rerun, reason? }`; `reject` requires a reason; validating with a failed blocking rule requires a reason too.
5. 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_audit` row (test)

**Out of scope** Building cycles (T41), Scanner AI write-up (T24).
