feat: add blockrun_scrapecheck, verifying a held web value against its live source page - #104
feat: add blockrun_scrapecheck, verifying a held web value against its live source page#104FieldmodeLLC wants to merge 2 commits into
blockrun_scrapecheck, verifying a held web value against its live source page#104Conversation
…live source page Typed structured tool per the CONTRIBUTING design rule (2 endpoints, fixed input contract). ScrapeCheck independently re-fetches the page and returns an ed25519-signed pass/fail/unverifiable verdict; a claim is never certified unless the re-fetched page contains it. Tiers: verify ($0.01 base) and presence ($0.002 base), both + tx fee via withTxFee. Registered in profiles (full + research), README tools row, count pins bumped 20->21 (profiles tests, brand-numbers.json, badge + alt). Gateway dependency stated in the PR description: routes /v1/scrapecheck/verify(-presence) forward to scrapecheck.fly.dev, which speaks x402 v2 with no API key on either side.
VickyXAI
left a comment
There was a problem hiding this comment.
Thanks for this — the tool code itself is a faithful copy of exa.ts (budget gate, withTxFee, gate.release() in finally, recordSpending after the call), the description stays under the 30-line rule, and you pinned every count surface including the two most people miss. I verified on your branch: tsc --noEmit clean, node scripts/sync-brand-numbers.mjs --check clean, and 341/341 tests pass — your 4 Solana wallet failures don't reproduce here, so they were local wallet state as you suspected.
Requesting changes on five things. The first two are why this can't land yet; the next three are conventions this repo already established elsewhere.
1. The gateway routes don't exist yet — blocker
You flagged this in the description, so no surprise, but recording the probe:
$ curl -s -o /dev/null -w '%{http_code}' -X POST https://blockrun.ai/api/v1/scrapecheck/verify
404
$ curl -s -o /dev/null -w '%{http_code}' -X POST https://blockrun.ai/api/v1/scrapecheck/verify-presence
404
$ curl -s -o /dev/null -w '%{http_code}' -X POST https://blockrun.ai/api/v1/exa/search
402
Both routes return a 404 HTML page, not a 402 challenge. Merging today ships a 21st tool that errors on every call for every user, while the README hero, the badge and the comparison table all advertise it. Your offer to hold the PR until the routes exist is the right sequencing — let's do that.
2. mcp.tools has to be bumped upstream in the same change
brand-numbers.json here is a snapshot of https://blockrun.ai/brand/numbers.json, which still publishes mcp.tools: 20. scripts/sync-brand-numbers.mjs writes the whole fetched object on --refresh with no key-level merge, so the next fan-out run reverts this file to 20 while ALL_TOOLS.length stays 21, and test/brand-numbers.test.ts fails CI on a PR that has nothing to do with this one. That test's own header names the companion step:
When it fails: confirm the change is intended, update
brand-numbers.json, and update the same number in blockrun'ssrc/app/brand/numbers.json/route.ts.
That second file is in a repo you can't reach, so it's on us — noting it here so it lands in the same window rather than after.
3. url skips the SSRF guard — src/tools/scrapecheck.ts
url is a bare z.string() and { url, claim, asked } goes straight to the gateway. src/tools/video.ts handles the identical shape and explains why it can't:
This process never fetches these URLs — the GATEWAY's fetcher does — so this is defense-in-depth plus a saved round trip: a URL pointing at localhost / the metadata endpoint / the private network was previously forwarded, quoted, and PAID for before failing (or worse, succeeding) server-side.
As written, file://…, http://localhost, http://169.254.169.254/, and wildcard-DNS forms like http://127.0.0.1.nip.io/ all get forwarded and billed. Please mirror the video/image pattern: parse the URL, require http:/https:, and run isBlockedFetchHostResolved(parsed.hostname) before reserveBudget. src/utils/ssrf.ts and test/ssrf.test.ts already have the primitives.
4. No Base-only guard — src/tools/scrapecheck.ts
The header comment says settlement goes to ScrapeCheck's Base treasury, but getClient() returns the Solana client whenever the session chain is Solana, and sol.blockrun.ai won't carry these routes. src/tools/price.ts and src/tools/realface.ts both fail closed with an actionable message:
if (getChain() !== "base") {
return { content: [{ type: "text", text: formatError("… settles on Base only. Switch BlockRun to Base (run blockrun_wallet with action:chain chain:base) and fund the Base wallet with USDC.") }], isError: true };
}Same guard here, before the budget reservation.
5. Register the estimator in scripts/verify-prices.ts
verify:prices is the only thing standing between us and a silent reprice — the gateway can change a price with no MCP release, so the drift only ever shows up in production. estimateScrapecheckCost isn't imported or probed there. A base price set outside our own catalog is exactly the case that script exists for. Two rows once the routes are live:
{ label: "scrapecheck/verify", path: "scrapecheck/verify", body: { url: "https://example.com", claim: { price: "$1" }, asked: "price?" }, expected: estimateScrapecheckCost("verify") },
{ label: "scrapecheck/presence", path: "scrapecheck/verify-presence", body: { url: "https://example.com", claim: { price: "$1" }, asked: "price?" }, expected: estimateScrapecheckCost("presence") },Worth saying: the $0.012 / $0.0040 in the README is your base plus our flat fee. Our gateway sets its own published price per route, so treat those figures as provisional until the route quotes a real payment-required header.
Two smaller notes
The description promises verification the code doesn't do. It tells the model verdicts are ed25519-signed and offline-verifiable and that presence never returns pass, then returns the vendor payload verbatim through asStructuredContent with no response schema, no pinned public key, no signature check, and no binding of the verdict to the url/claim/asked it was issued for. A tool whose entire value is "don't trust the last hop" shouldn't ask the agent to trust this one on its word. Either validate the shape and check the signature against a key pinned in this repo, or soften the copy to describe what the tool actually returns.
No test. Every money-touching tool here has one (test/image-cost.test.ts, test/modal-cost.test.ts, test/search-cost.test.ts, …). A small test/scrapecheck-cost.test.ts covering both tiers, the unknown-tier fallback, and the budget-gate rejection path would round this out.
Happy to keep this open while we sort the gateway routes and the upstream mcp.tools bump on our side. Items 3, 4 and 5 are self-contained and can go in now.
…put the route under verify:prices Maintainer follow-up on BlockRunAI#104. The tool itself was a faithful copy of exa.ts; these are the three conventions a pass-through partner route has to carry, plus the test that pins them. SSRF. `url` was a bare z.string() forwarded raw. This process never fetches it — the gateway does, then ScrapeCheck does — which is exactly the case blockrun_video and blockrun_image already guard, and the comment there says why: a URL pointing at localhost / the metadata endpoint / the private network was "forwarded, quoted, and PAID for before failing (or worse, succeeding) server-side". file://, localhost, 169.254.169.254 and wildcard-DNS forms like 127.0.0.1.nip.io all went through. Now: scheme check plus isBlockedFetchHostResolved(), BEFORE reserveBudget, so a rejected URL never touches the ledger. Chain. Settlement goes to ScrapeCheck's Base treasury and sol.blockrun.ai does not carry these routes, but getClient() returns the Solana client whenever the session chain is Solana. Now fails closed with the same actionable message price.ts and realface.ts use, ahead of the SSRF check so the answer a Solana user gets is "switch chains", not "that host is private". verify:prices. The base price here is set by ScrapeCheck rather than by our own catalogue, so it can move with no BlockRun deploy — the exact drift that script exists to catch, and it had no scrapecheck rows. Both routes are registered; they report `? no 402 (HTTP 404)` until the gateway routes exist, which is the honest signal (unreachable is counted separately and never reads as passing). Also: the description told the model verdicts were "verifiable offline" while the handler returned the vendor payload verbatim — no schema, no pinned key, no signature check. Softened to say what the tool actually does, since a tool whose whole value is "don't trust the last hop" should not ask the agent to trust this one on its word. README gains the Base-only note alongside the other Base-only tools. test/scrapecheck.test.ts: 8 cases — both prices, unknown-tier fails closed to the expensive tier, five SSRF forms rejected with zero network and zero spend, the happy path booking exactly once, presence hitting the cheap route, the Solana guard, and the budget gate stopping the call rather than merely recording it. Verified: tsc clean, sync-brand-numbers --check clean, 349/349 tests, stdio handshake lists 21 tools, verify:prices exits 0 with 0 under-reserving.
|
Pushed items 3, 4 and 5 straight onto this branch (thanks for leaving maintainer edits on) —
Verified on the branch: That leaves the two items that are ours, not yours, and they're the reason this stays open:
For (1) the shape we'd use is the Surf one — a partner entry in the gateway's
Send those to the address on file and we'll wire the routes. Everything else is already merged onto this branch. |
|
Hi,
Thanks for pushing those yourself, and for the tests. Way beyond what the
review needed to be, and it saved us a day! Much appreciated!
The SSRF catch was the useful one, honestly. Sent us to go check the same
thing on our own endpoint, and we had it: our checker's pre-fetch wasn't
running the guard our engine's re-fetch has always used. Found it, fixed
it, shipped it. Thanks for that!
On the key, yes please. It's at https://scrapecheck.fly.dev/pubkey. Pin it
from there rather than from this email. Active key is k2, ends GeSU= if you
want to sanity check you got the right one.
Heads up before you pin: we rotated on 6 August after a credentials
exposure (reason published), and we'll rotate again at some point. That
endpoint carries the history, so pin the URL too, not just the key. Better
still, verify against whichever pinned key matches the verdict's key_id and
fail closed on an id you don't know, rather than fetching a fresh one at
runtime. Saves you an outage and closes the obvious swap.
When we send the validation commit we'll also bind the verdict to the url,
claim and asked it was issued for, so a signature that's valid for some
other page can't pass as this one.
Settlement address is 0x455F20eC147477CAFcd6D77C1604529f3c4Ff665. Don't
take my word for it. POST to https://scrapecheck.fly.dev/verify unpaid and
read payTo off the 402, which is what your gateway reads at settlement
anyway. If it doesn't match what I've written, trust the 402.
On price: quote against the published base, $0.01 per full check, $0.002
per presence, your margin on top. If that's awkward on your side let me
know and I'll look at it, I just don't want to move the number while you're
building against it.
One thing I want to get straight before the routes go live. When you say
the price carries your margin and gets reconciled off-chain, which way does
that run and how often? If we're holding your margin between settlements
I'd rather have it written down somewhere now than sort it out later.
Thanks again!
… Message ID: ***@***.***>
|
|
Correction to my earlier comment, and it's on me. I wrote that the integration shape "we'd use" would be a partner entry settling to your Base treasury, and asked you to send the address and pricing so we could wire the routes. That was further than I had standing to go. It reads as a decision to onboard ScrapeCheck, and no such decision has been made — integration and revenue-share calls on this project are the owner's, and this one hasn't reached them. So, plainly: there is no commitment here, and no gateway work is queued. Please don't send a treasury address on the strength of that comment. What stands unchanged is the code review. The hardening I pushed to this branch in This PR stays open but parked, pending an owner decision on whether BlockRun takes third-party paid routes at all. I'm not going to speculate on the timing or the outcome. Apologies for implying otherwise — you're owed a straight answer rather than a maybe dressed up as a next step. |
|
Thanks for closing it cleanly rather than letting it sit.
For what it's worth, the review was very much appreciated. Your SSRF note
sent us to check the same class on our own endpoint and we found a live one
in production.
Good luck with the build. Thanks again.
…On Wed, Aug 19, 2026, 4:10 AM VickyXAI ***@***.***> wrote:
Closed #104 <#104>.
—
Reply to this email directly, view it on GitHub
<#104?email_source=notifications&email_token=CJZMEUJBXG4TIGETN7DN2W35KVOIJA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEOJWGY4DSNZZGQYTNJTSMVQXG33OUZQXK5DIN5ZKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#event-29668979416>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/CJZMEUNEIUK62NPOP6YBGKT5KVOIJAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMZSGYYDENRUGM5US43TOVSTWNJRGU2TCMZTGU2DJILWAI>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/CJZMEUIVLJ3WJADLNYQTQ2T5KVOIJA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEOJWGY4DSNZZGQYTNJTSMVQXG33OUZQXK5DIN5ZKKZLWMVXHJKTGN5XXIZLSL5UW64Y>
and Android
<https://github.com/notifications/mobile/android/CJZMEUK5YLFANNHOBVGGFHD5KVOIJA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEOJWGY4DSNZZGQYTNJTSMVQXG33OUZQXK5DIN5ZKKZLWMVXHJLTGN5XXIZLSL5QW4ZDSN5UWI>.
Download it today!
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
Adds a typed structured tool (per the CONTRIBUTING design rule: two endpoints, fixed input contract, no long catalog) for ScrapeCheck (https://scrapecheck.fly.dev). The use case: an agent holds a value that came from somewhere else (a search result, a scraper, another tool) and needs to know it is on the source page right now before acting on it. ScrapeCheck re-fetches the page independently and returns an ed25519-signed pass/fail/unverifiable verdict. A claim is never certified unless the re-fetched page contains it, and anything unconfirmed is unverifiable, never pass. Verdicts verify offline against the published key, so the agent (or its user) doesn't have to trust the service at runtime.
This composes with tools you already ship: blockrun_exa finds, blockrun_scrapecheck confirms before the agent acts or pays on what it found.
What's in the diff:
One dependency on your side, stated plainly: the tool calls /v1/scrapecheck/verify and /v1/scrapecheck/verify-presence, which need gateway routes forwarding to scrapecheck.fly.dev. Same shape as your Surf and Exa routes, with one difference that makes it easy: there is no API key on either side. ScrapeCheck speaks x402 v2 and is listed in the CDP Bazaar catalog; the gateway can settle per call to our Base treasury exactly as it does for Surf. The first 100 checks per client are free (header X-Use-Free-Allowance: yes), so your integration testing costs nothing. Happy to adjust the route shape, pricing presentation, or tool copy to whatever your gateway conventions need, or to hold this PR until the routes exist if you'd rather sequence it that way. Contact: sales@fieldmodesolutions.com.