Skip to content

feat(tools): add StallRowPageMetadataTool — first x402 pay-per-call tool - #9296

Open
QuincyGB wants to merge 4 commits into
crewAIInc:mainfrom
QuincyGB:feat/stallrow-page-metadata-tool
Open

QuincyGB wants to merge 4 commits into
crewAIInc:mainfrom
QuincyGB:feat/stallrow-page-metadata-tool

Conversation

@QuincyGB

@QuincyGB QuincyGB commented Nov 9, 2026 •

Copy link
Copy Markdown

Resolves #9295

What this adds

StallRowPageMetadataTool — a new tool in crewai_tools that fetches one web page's declared metadata in a single call: title, description, canonical URL, OpenGraph and Twitter card fields, document language, favicon, the h1/h2 heading outline, and word count. It wraps StallRow's live POST /v1/page-metadata endpoint and mirrors the layout of the existing API tools (pydantic args schema, env_vars declaration, package README, docs page, unit tests, regenerated tool.specs.json entry).

from crewai_tools import StallRowPageMetadataTool

tool = StallRowPageMetadataTool()
result = tool.run(url="https://www.x402.org/")
# {"url": ..., "title": "x402 — internet-native payments", ...,
#  "headings": {"h1": ["x402"], "h2": ["How it works"]}, "word_count": 120}

Why

CrewAI has no pay-per-call tool today — there is no x402 support anywhere in the codebase. API-key tools force users to create accounts and manage subscriptions for utilities they may call a handful of times. With x402, payment is the auth: an agent pays $0.005 USDC per call, only when it calls, with no signup. Page metadata is the highest-frequency shape for this (an agent checks a page's identity and outline before deciding whether the full content is worth fetching), which makes it the right first tool of this class.

How payment works

  • The endpoint answers HTTP 402 with an x402 v2 challenge; the bundled x402_client.py helper selects the exact/Base requirement, signs an EIP-3009 TransferWithAuthorization as EIP-712 typed data, and retries with the PAYMENT-SIGNATURE header. The service's facilitator settles USDC on Base server-side. The challenge's extensions (Bazaar discovery declaration) are echoed into the payment payload, as facilitator catalogues index from the payload.
  • Price: $0.005 USDC per call. A hard per-call cap (the tool's max_price_usdc parameter, or STALLROW_MAX_PRICE_USDC) defaults to exactly that price and refuses any higher price before anything is signed.
  • The payer key is read from the STALLROW_WALLET_PRIVATE_KEY environment variable only — never a tool parameter, never logged. With no wallet configured, the tool fails closed with an explanation and pays nothing. The tool module imports and constructs fine without the signer installed; eth-account is imported lazily at payment time and declared via package_dependencies plus a new crewai-tools[stallrow] optional-dependencies extra.

Disclosure

I operate the StallRow service this tool calls — this is a first-party service tool, in the same sense as the Apify and Firecrawl tools already in this directory. The endpoint is live, listed in the CDP Bazaar, and priced at $0.005/call. I'm disclosing the affiliation plainly so maintainers and users can weigh it; the spend cap and fail-closed defaults are designed so an agent can never be charged more than the stated price for a call.

Tests

  • 10 new unit tests (lib/crewai-tools/tests/tools/test_stallrow_page_metadata_tool.py, HTTP fully mocked): the 402 → sign → retry flow (asserting the decoded payment payload: accepted amount/payTo, EIP-3009 authorization fields, signature shape, bazaar-extension echo), challenge parsing from the response body when the header is absent, cap refusal before signing (default cap, env override, tool-parameter override), fail-closed behaviour with no wallet configured, an upstream 503 after payment surfaced as an error, and a second 402 after payment treated as an error.
  • tool.specs.json regenerated with the repo's own generator (one new entry, no unrelated churn).
  • ruff check and ruff format --check clean under the repo config; mypy (strict, repo config) clean on the new package.
  • Verified end-to-end against the production endpoint with a real $0.005 payment from a test wallet: the tool returned the page's metadata and the payment settled on Base (payer −$0.005 USDC, service payTo +$0.005 USDC, read back on-chain).

Notes for maintainers

  • Docs: English page added under docs/edge/en/tools/search-research/ and registered in docs/docs.json (edge nav only). Translations (ar, ko, pt-BR) not synced in this PR — happy to add them or leave them to the docs pipeline, as you prefer.
  • Follow-ups I can add as separate PRs if this lands well: sibling tools for StallRow's url-unshorten ($0.005) and page-to-markdown ($0.01) routes, sharing the same payment helper.

Note

High Risk
Adds private-key handling and on-chain USDC payments for a third-party endpoint; mistakes or misconfiguration could spend wallet funds, though caps and fail-closed paths mitigate surprise charges.

Overview
Introduces CrewAI’s first pay-per-call tool, StallRowPageMetadataTool, which returns structured page metadata (title, OG/Twitter, headings, word count, etc.) from StallRow’s /v1/page-metadata endpoint.

Payment uses a new x402_client (402 challenge → EIP-3009 USDC signing on Base → PAYMENT-SIGNATURE retry) with fail-closed behavior: no wallet means no payment, hard per-call cap (default $0.005), USDC-only settlement, validity-window limits, and no double-pay if the paid response is lost. Signing depends on optional crewai-tools[stallrow] (eth-account, lazy import).

Also adds docs nav/page, package exports, regenerated tool.specs.json, broad unit tests (mocked HTTP), and uv.lock entries for the Ethereum stack.

Reviewed by Cursor Bugbot for commit d64ab00. Bugbot is set up for automated code reviews on this repo. Configure here.

@strix-security

strix-security Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Strix Security Review

Warning

This pull request has 2 commits after the last Strix review (7173e36). Strix has not reviewed these changes.
Automatic review on push is off for this repository. To review the latest changes, tag @strix-security in a comment, or turn on re-review on push.

✅ No open security findings.

PR summary: Adds `StallRowPageMetadataTool` to `crewai_tools`, a pay-per-call tool that fetches a page's title, description, OpenGraph/Twitter fields, heading outline and word count from a StallRow endpoint, paying $0.005 USDC per call over x402 on Base via a new bundled `x402_client` that signs an EIP-3009 authorization from a wallet key in `STALLROW_WALLET_PRIVATE_KEY`. Packaging (`crewai-tools[stallrow]` extra), exports, docs, unit tests for the payment/cap paths and a regenerated `tool.specs.json` entry are included.

Review summary

I reviewed the full diff of the new StallRow pay-per-call tool and its x402 payment client, including the EIP-3009/EIP-712 signing path, the cap and asset enforcement, the private-key handling, the packaging/exports, and the regenerated tool spec. The payment gates hold up: the asset is pinned to USDC on Base, the network is pinned to eip155:8453, the amount is refused before signing when it exceeds the cap, and with no wallet configured the client fails closed; a hostile 402 challenge (over-cap, non-USDC, foreign network, malformed fields) was probed with a real signer and never produced an over-cap or wrong-domain signature. The payer key is read from the environment only, is never a tool field, is never logged, and does not appear in request bodies/headers, tool output, exception text or the generated spec. The one finding raised during the review (redirect following forwarding the signed PAYMENT-SIGNATURE off-origin) was independently validated and withdrawn: it requires control of the payment endpoint's response, which already allows the same outcome, and the forwarded authorization is single-use and bound to the legitimate recipient.

A few non-blocking hardening notes for the author: payTo and maxTimeoutSeconds come straight from the challenge with no allowlist or upper bound (x402_client.py), and amount is parsed twice (cap check vs. signed value) plus never checked for a negative value — all currently safe only because the ABI encoder or the upstream gate catches them; allow_redirects=False on both POSTs would be cheap defence in depth; X402Client.from_env is called outside _run's try, so a malformed STALLROW_MAX_PRICE_USDC raises decimal.InvalidOperation through the agent loop instead of returning the tool's error string; and there is no aggregate spend ceiling or repeat dedup, so per-run cost is bounded by max_iter rather than by any money limit, which is worth noting in the docs since the PR describes the cap as making cost predictable by construction.

Also note the regenerated uv.lock carries an unrelated workspace-wide parsimonious 0.11.0 → 0.10.0 downgrade (forced by eth-abi), which also affects the existing singlestore extra — worth calling out in the PR description since it contradicts the "no unrelated churn" note.

Ruled out (7)
  • Can a hostile 402 challenge make the client sign an over-cap amount, a non-USDC asset, or a foreign chain's authorization?
    Probing with a real eth-account signer showed the asset is pinned to Base USDC, the network to eip155:8453 and the amount refused before signing when above the cap; float/bool/negative amounts and malformed requirements all raise before sign_typed_data, so exactly one unpaid POST occurs and no sign…
  • Can the payer private key leak to an agent, a log, a trace, a tool spec or the remote party?
    The environment value has only three touch points (read, store, sign), EnvVar metadata carries no value and is never consumed at runtime, and empirical probes showed the key absent from every outbound body/header, return value and full exception chain.
  • Does the framework's 3x tool retry turn one agent action into multiple on-chain payments?
    `_run` catches every failure that can occur after the paid POST (X402Error subclasses and requests.RequestException) and returns a string, so nothing raises after money moves and the retry cannot re-sign; the remaining uncaught exceptions all occur before the key is read.
  • Can a redirect from the payment endpoint leak the signed PAYMENT-SIGNATURE to a third party and let it be reused?
    Requests does forward the header cross-origin, but only the endpoint itself can emit the redirect — a party that already controls the challenge and response body — and the forwarded EIP-3009 authorization is single-use and bound to the legitimate payTo and value, so nothing can be repurposed.
  • Can the challenge's payTo or maxTimeoutSeconds be abused to redirect funds or extend the authorization's validity?
    An endpoint that names its own payTo gains nothing beyond the capped payment it was already being paid, and an unbounded validBefore does not change the capped value or the single-use nonce.
  • Does the tool let a prompt-injected agent drain the wallet with repeated paid calls?
    Per-call cost is hard-capped and the operator must opt in with a funded key, so the absence of an aggregate budget or repeat dedup is a design/hardening property bounded by max_iter, not an attacker-gainable escalation.
  • Do a malformed STALLROW_MAX_PRICE_USDC or a hostile challenge field crash the agent instead of failing closed?
    `decimal.InvalidOperation`/`KeyError`/`ValueError` do escape `_run`, but every one of them occurs before the key is read or anything is signed, so it is a robustness gap on operator-controlled input rather than a payment or disclosure risk.

Review 1 on this PR · 7173e36 · requested Nov 9 15:26 UTC when the PR was opened


Reviewed by Strix
Re-run review · Configure security review settings

@mintlify

mintlify Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Preview approval required

This pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit d64ab00.

Approve preview

The x402 challenge's maxTimeoutSeconds was signed into the EIP-3009 validBefore with no client ceiling, so an endpoint could mint an authorization settleable for years and keep it after a failed response. Refuse any window outside 1-300 seconds before signing, with a refusal test.
If the paid x402 retry's response is lost in transport, settlement
is unconfirmed and the metadata was not delivered. The client now
re-reads the challenge with one unpaid probe, names any payment
terms the fresh challenge changed (network, asset, payTo, amount),
and never authorizes a second payment for a call whose first
payment may still settle. The new X402ResponseLostError
distinguishes payment_confirmed from metadata_delivered instead of
reporting the paid call as a plain failure.

Adds the response-loss test discussed in crewAIInc#9295 (13 tests total,
mock 402 + mocked transport only).
@QuincyGB

Copy link
Copy Markdown
Author

Follow-up on the response-loss case raised in #9295: the test is in, as test_response_loss_after_payment_fails_closed_on_changed_challenge (commit d64ab00).

It mocks the flow end to end — 402 challenge, paid retry whose response is lost in transport (ConnectionError, no response at all), then the fresh 402 on re-read — and asserts the client:

  • re-reads the challenge with an unpaid probe before doing anything else,
  • refuses to authorize a second payment when the fresh challenge's terms changed (the test changes amount 5000 -> 6000; network, asset and payTo are compared the same way), with exactly one signed request across the whole flow, and
  • surfaces the two states separately — payment_confirmed: unknown, metadata_delivered: false — instead of reporting the paid call as a plain failure.

To make that behavior real, X402Client now raises a dedicated X402ResponseLostError on transport loss of the paid request and fails closed: it never re-signs for a call whose first payment may still settle. The stable operation_id / reconciliation piece stays out of this PR, as discussed in #9295. Suite is 13/13 for this file; ruff and mypy clean on the changed files.

This branch has not been deployed

No deployments
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.

[FEATURE] Add StallRowPageMetadataTool — an x402 pay-per-call page-metadata tool

1 participant