Skip to content

feat: add blockrun_scrapecheck, verifying a held web value against its live source page - #104

Closed
FieldmodeLLC wants to merge 2 commits into
BlockRunAI:mainfrom
FieldmodeLLC:add-scrapecheck-tool
Closed

feat: add blockrun_scrapecheck, verifying a held web value against its live source page#104
FieldmodeLLC wants to merge 2 commits into
BlockRunAI:mainfrom
FieldmodeLLC:add-scrapecheck-tool

Conversation

@FieldmodeLLC

Copy link
Copy Markdown

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:

  • src/tools/scrapecheck.ts: typed tool, two tiers (verify $0.01 base, presence $0.002 base, both plus tx fee via withTxFee), budget gate and recordSpending following exa.ts
  • registered in profiles (full and research), README tools row, count pins 20 to 21 (profiles tests, brand-numbers.json, hero badge and alt text)
  • typecheck, build, tests, and the stdio smoke test all run; the 4 Solana-wallet test failures on my machine reproduce identically on unmodified main (local wallet state), so they are environmental, not from this change

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.

…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 VickyXAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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's src/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.
@VickyXAI

Copy link
Copy Markdown
Contributor

Pushed items 3, 4 and 5 straight onto this branch (thanks for leaving maintainer edits on) — ff6f026. Nothing to do on your side for those; please pull before your next push.

  • SSRF — scheme check + isBlockedFetchHostResolved() on url, ordered before reserveBudget so a rejected URL never touches the ledger. file://, localhost, 169.254.169.254 and 127.0.0.1.nip.io were all being forwarded and billed.
  • Base-only guardgetChain() !== "base" returns the same switch-chain message price.ts and realface.ts use. Placed ahead of the SSRF check so a Solana user gets "switch chains" rather than "that host is private".
  • verify:prices — both routes registered. They report ? no 402 (HTTP 404) today, which is the honest signal; unreachable is counted separately and never reads as passing. Run exits 0, 0 under-reserving.
  • test/scrapecheck.test.ts — 8 cases covering both prices, unknown-tier failing closed to the expensive tier, five SSRF forms with zero network and zero spend asserted, 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.
  • Copy — the description said verdicts were "verifiable offline" while the handler returned your payload verbatim. Softened to: the verdict carries an ed25519 signature the caller can check against your published key, and this tool does not check it. If you'd rather have the real thing, ship us the public key and I'll pin it in the repo and validate before returning — happy to take that as a follow-up commit from you.

Verified on the branch: tsc clean, sync-brand-numbers --check clean, 349/349 tests, stdio handshake lists 21 tools.

That leaves the two items that are ours, not yours, and they're the reason this stays open:

  1. The gateway routes. /api/v1/scrapecheck/* still 404s. Our side.
  2. mcp.tools upstream. Has to flip 20→21 in the same window the routes go live, or the fan-out reverts this repo's snapshot and breaks CI on an unrelated PR.

For (1) the shape we'd use is the Surf one — a partner entry in the gateway's x402-partner.ts with x402 settling directly to your Base treasury, price carrying our margin, reconciled off-chain. To build it we need two things from you:

  • the Base treasury address payments should settle to (the PR says "our Base treasury" but doesn't give the address, and I'm not going to guess a payout destination), and
  • confirmation of the per-call base you want us to quote against — $0.01 / $0.002 as published, or something else once our margin is on top.

Send those to the address on file and we'll wire the routes. Everything else is already merged onto this branch.

@FieldmodeLLC

FieldmodeLLC commented Aug 18, 2026 via email

Copy link
Copy Markdown
Author

@VickyXAI

Copy link
Copy Markdown
Contributor

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 ff6f026 — SSRF guard on url, the Base-only chain guard, verify:prices coverage, and test/scrapecheck.test.ts — was ordinary review work on a contribution that was already well built, not a step toward shipping it. It stays on the branch either way and costs you nothing.

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.

@VickyXAI VickyXAI closed this Aug 19, 2026
@FieldmodeLLC

FieldmodeLLC commented Aug 19, 2026 via email

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants