Skip to content

Add pulsenetwork-data template: agent buys live data mid-task via x402 - #13

Open
GTCC777 wants to merge 4 commits into
browser-use:mainfrom
GTCC777:add-pulsenetwork-data-template
Open

Add pulsenetwork-data template: agent buys live data mid-task via x402#13
GTCC777 wants to merge 4 commits into
browser-use:mainfrom
GTCC777:add-pulsenetwork-data-template

Conversation

@GTCC777

@GTCC777 GTCC777 commented Aug 9, 2026

Copy link
Copy Markdown

What

A template showing a browser-use agent buying live external data mid-task with x402 micropayments (USDC on Base): the agent searches a catalog of 950+ pay-per-call endpoints, checks the exact price with a free bare-402 quote, then pays a few cents for the call it needs (the demo buys a $0.015 crypto token-safety verdict before evaluating a hyped token).

Follows the tools_template.py pattern: three custom @tools.action functions (pulse_catalog free search, pulse_price free quote, pulse_buy paid call), plus a runnable main().

Why

Your cloud accepts x402 since last week, so agents in this ecosystem increasingly carry funded wallets. This template shows the complementary direction: the same wallet buying data the agent needs mid-run, with no API keys or signups.

Safety properties (in code, not prompts)

  • Per-call price cap (default $0.50) and session budget (default $2.00), checked against the free 402 quote before anything settles
  • Host allowlist: the pay tool refuses any non-PulseNetwork host, so page content cannot redirect spend
  • The wallet key lives in an env var (PULSE_WALLET_KEY); the LLM never sees it
  • Payments via the official x402 Python SDK (x402[httpx,evm])

Tested

  • Tools register on browser-use 0.13.7 (24 default + 3 = 27 actions)
  • Live end-to-end payment verified on Base mainnet ($0.015 settle, JSON verdict returned)
  • Quote/caps/host-guard paths unit-checked

Disclosure: I operate the PulseNetwork endpoints the template calls. The catalog/quote tools are free; only pulse_buy spends, capped as above.


Summary by cubic

Adds the pulsenetwork-data template so an agent can buy live external data mid-task via x402 micropayments (USDC on Base). Ships as a complex template with README, .env.example, and pyproject.toml, and tightens spending by binding caps to the signed 402 challenge with Base USDC pinning and stricter allowlists.

  • New Features

    • Three tools: pulse_catalog (free search), pulse_price (free 402 quote), pulse_buy (paid call, returns JSON).
    • Safer pay path: per-call cap and session budget enforced as an x402 policy bound to the real 402 challenge; spend recorded from the signed amount; asset pinned to Base USDC; lock prevents concurrent overspend; https-only PulseNetwork host allowlist applied to all outbound tools; placeholder or invalid PULSE_WALLET_KEY refuses cleanly.
    • Accurate quotes: pulse_price only quotes Base USDC options the buy path can actually pay; unsupported assets return a clear “No USDC-on-Base x402 quote” message.
    • Better discovery: pulse_catalog uses the free /api/catalog JSON endpoint and returns complete URLs with prices and parameters; clear error if the catalog is unreachable.
    • Complex template in templates.json: ships main.py, pyproject.toml, .env.example, README.md, and guided next steps so deps install and wallet setup is clear.
  • Dependencies

    • Adds x402[httpx,evm], httpx, eth-account, and python-dotenv (template includes browser-use).

Written for commit 010b412. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread pulsenetwork_data_template.py Outdated
Comment thread pulsenetwork_data_template.py Outdated
Comment thread templates.json Outdated
…e signed challenge

Addresses all three cubic findings.

templates.json (P2): promoted from a bare single-file template to a complex
one. A single .py copy left users with no pyproject.toml (so x402 was never
installed) and no .env example for the wallet key, so the first 'uv run'
failed on ImportError. Now ships main.py, pyproject.toml.template pinning
x402[httpx,evm], .env.example.template, README.md and the shared gitignore,
plus next_steps that walk through wallet setup, 'uv sync' and the run.

pulse_catalog (P1): was grepping llms-full.txt line by line, so the task's own
query ('token safety') matched no line at all, and any line that did match
carried a relative route with the base URL somewhere else in the file. It now
queries the free /api/catalog JSON endpoint and returns each match as a
complete URL with its price and its documented parameters, so pulse_buy can be
called directly from the result. Catalog-side ranking was fixed to match on
word boundaries with query-side aliases, so 'token safety' now returns the
token scanners. No silent fallback: if the catalog is unreachable the tool
says so rather than answering from a stale grep.

pulse_buy (P1): the preflight quote was advisory only, since x402HttpxClient
went on to sign whatever challenge the second request returned. The quote is
gone from the buy path. The per-call cap and the remaining session budget are
now an x402 payment policy, so they are checked against the challenge that is
actually signed, and a payload is never created when it does not fit. Spend is
recorded from the signed amount via an after-payment-creation hook, and
accumulated rather than overwritten so a retry can never undercount. A lock
around the buy path stops two concurrent calls from both spending the last of
the budget, and an invalid or missing wallet key now refuses cleanly instead
of raising.

Verified against the live fleet: caps refuse without signing, and a real
$0.015 USDC settle on Base returns the token verdict.
@GTCC777

GTCC777 commented Aug 9, 2026

Copy link
Copy Markdown
Author

All three findings were valid and are fixed in 099e06c.

templates.json (P2) — registered as a complex template. You were right that
browser-use init --template pulsenetwork-data would have copied one .py file
and then died on ImportError for x402. It now ships main.py,
pyproject.toml.template (pinning x402[httpx,evm], eth-account,
python-dotenv), .env.example.template, README.md and the shared
gitignore.template, with next_steps covering wallet setup, uv sync and the
run. validate_templates.py passes.

pulse_catalog (P1) — reproduced, and it was worse than the report. The
task's own phrasing, "token safety", matched zero lines of llms-full.txt, and
lines that did match carried a relative route with the base URL declared
elsewhere in the file. The tool now queries the free
/api/catalog
JSON endpoint and returns each match as a complete URL with its price and its
documented parameters, so the result can be handed straight to pulse_buy. I
also fixed the ranking behind that endpoint (word-boundary matching plus
query-side aliases, so intent words like "safety" reach product words like
"scan" and "honeypot") — "token safety" now returns the token scanners first. If
the catalog is unreachable the tool says so rather than quietly falling back to
a grep.

pulse_buy (P1) — real TOCTOU, and the quote is gone from the buy path. The
preflight quote never authorized anything: x402HttpxClient independently
signed whatever the second request's challenge said. The per-call cap and the
remaining session budget are now an x402 payment policy, so they are evaluated
against the challenge that is actually signed, and no payload is created when it
does not fit. Spend is recorded from the signed amount through an
after-payment-creation hook, accumulated rather than overwritten so a retry can
never undercount. Two smaller things went in alongside: a lock around the buy
path, so concurrent tool calls cannot both spend the last of the budget, and
clean refusals for a missing or malformed wallet key instead of a KeyError.

Verified against the live endpoints: a cap breach refuses with nothing signed,
and a real $0.015 USDC settle on Base returns the token verdict.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 7 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pulsenetwork-data/.env.example.template">

<violation number="1" location="pulsenetwork-data/.env.example.template:4">
P3: The documented env-check promise doesn't hold with this placeholder: README says leaving the key placeholder yields "Refused: PULSE_WALLET_KEY is not set", but this non-empty placeholder (`0xyour-throwaway-private-key`) passes the `if not key` guard and instead hits Account.from_key, producing a different message ("not a valid private key"). Either add an explicit placeholder sentinel check in main.py alongside the empty check, or correct the README refusal-table entry, so the template's placeholder and the documented refusal actually match.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread pulsenetwork-data/main.py
Comment thread pulsenetwork-data/main.py
# Wallet the agent pays from. Use a THROWAWAY key holding a few USDC on Base,
# never a wallet you keep funds in. This is the only credential you need:
# PulseNetwork has no accounts, no signup and no API keys.
PULSE_WALLET_KEY=0xyour-throwaway-private-key

@cubic-dev-ai cubic-dev-ai Bot Aug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The documented env-check promise doesn't hold with this placeholder: README says leaving the key placeholder yields "Refused: PULSE_WALLET_KEY is not set", but this non-empty placeholder (0xyour-throwaway-private-key) passes the if not key guard and instead hits Account.from_key, producing a different message ("not a valid private key"). Either add an explicit placeholder sentinel check in main.py alongside the empty check, or correct the README refusal-table entry, so the template's placeholder and the documented refusal actually match.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pulsenetwork-data/.env.example.template, line 4:

<comment>The documented env-check promise doesn't hold with this placeholder: README says leaving the key placeholder yields "Refused: PULSE_WALLET_KEY is not set", but this non-empty placeholder (`0xyour-throwaway-private-key`) passes the `if not key` guard and instead hits Account.from_key, producing a different message ("not a valid private key"). Either add an explicit placeholder sentinel check in main.py alongside the empty check, or correct the README refusal-table entry, so the template's placeholder and the documented refusal actually match.</comment>

<file context>
@@ -0,0 +1,12 @@
+# Wallet the agent pays from. Use a THROWAWAY key holding a few USDC on Base,
+# never a wallet you keep funds in. This is the only credential you need:
+# PulseNetwork has no accounts, no signup and no API keys.
+PULSE_WALLET_KEY=0xyour-throwaway-private-key
+
+# Model for the agent (any browser-use supported provider works)
</file context>
Fix with cubic

…ols, honour the placeholder

Second review round, all three valid.

Asset pinning (P1): the payment policy checked the network but not the asset,
so a Base challenge denominated in some other token would have passed a cap
counted in USDC's six decimals. The same atomic amount in an 8 or 18 decimal
token is a completely different amount of money. The policy now requires Base
USDC by address and refuses anything else.

Allowlist on the free tools (P2): pulse_price fetched whatever URL the model
handed it. The agent reads live web pages, so a page could have steered it into
probing a link-local or internal address, and a free tool with no allowlist
would have made that request. Both the host check and an https requirement now
live in one helper that every outbound path runs, not just the paying one.
pulse_catalog was never affected: it only ever sends the query as a parameter
to a fixed catalog URL.

Placeholder key (P3): the shipped .env placeholder is non-empty, so an unedited
copy reached Account.from_key and reported a malformed key rather than the
documented 'not set'. It is treated as unconfigured now, and the README
troubleshooting table covers both cases plus the allowlist refusal.

Verified: the metadata address, loopback, plain http and an off-domain host are
all refused by both free and paying tools; a synthetic non-USDC Base challenge
is dropped while real Base USDC is kept; and a live $0.015 settle still returns
the token verdict.
@GTCC777

GTCC777 commented Aug 9, 2026

Copy link
Copy Markdown
Author

All three valid again. Fixed in 1bee395.

Asset pinning (P1) — the sharp one. You are right, and it was worse than
network-versus-asset tidiness. The caps are counted in USDC's six decimals, so
an unpinned asset makes them meaningless: 500000 atomic units is $0.50 of USDC
but roughly $500 of an 8-decimal token, and the cap would have waved it through.
The policy now requires Base USDC by contract address and refuses anything else
with its own message. Verified with a synthetic non-USDC Base challenge, which
is dropped, alongside a real one, which is kept.

Allowlist on the free tools (P2) — real, fixed. pulse_price did fetch
whatever URL the model handed it, and given the agent is reading live pages
while it works, that is a page-steerable outbound probe. The host check plus an
https requirement now live in one helper that every outbound path runs, so the
free and paying tools share the trust boundary. 169.254.169.254, localhost,
plain http and an off-domain host are all refused by both.

One correction to the report: pulse_catalog was never part of this. It takes a
search string, not a URL, and sends it as a query parameter to a fixed catalog
endpoint. There is no model-supplied host in that path.

Placeholder key (P3) — fair catch. The shipped placeholder is non-empty, so
an unedited .env reached Account.from_key and reported a malformed key
instead of the documented "not set". It is treated as unconfigured now, and the
README troubleshooting table covers the placeholder case, the genuinely
malformed case and the allowlist refusal separately.

Re-verified end to end after the changes, including a live $0.015 USDC settle on
Base that still returns the token verdict.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread pulsenetwork-data/main.py
Valid, and it is a hole I opened myself last round. Pinning the asset in the
payment policy left the quote path matching on network alone, so a Base
challenge denominated in some other token would have had its amount divided by
USDC's decimals and printed as a confident dollar figure, for a challenge the
buy path then refuses. Wrong number, and an inconsistency between two tools
that are supposed to describe the same thing.

_quote_usd now requires the same Base USDC asset the policy requires, so an
unsupported challenge returns no quote rather than a meaningless one. It also
keeps scanning the accepts list instead of taking the first Base entry, so a
challenge offering a non-USDC option ahead of a USDC one is priced from the
option that can really be paid.

Verified against crafted 402 responses covering Base USDC, a non-USDC Base
asset, USDC on another network and a mixed list, plus a live quote against a
real endpoint that still returns $0.015.
@GTCC777

GTCC777 commented Aug 9, 2026

Copy link
Copy Markdown
Author

Valid, and it is a hole I opened myself last round. Fixed in 010b412.

Pinning the asset in the payment policy left _quote_usd still matching on
network alone, so a Base challenge denominated in some other token would have
had its amount divided by USDC's six decimals and printed as a confident dollar
figure, for a challenge pulse_buy was then going to refuse. Two tools
describing the same challenge and disagreeing about it, with the wrong one being
the tool that speaks first.

_quote_usd now requires the same Base USDC asset the policy requires, so an
unsupported challenge returns no quote instead of a meaningless one. One thing
beyond the report: it keeps scanning the accepts list rather than returning on
the first Base entry, so a challenge that offers a non-USDC option ahead of a
USDC one is priced from the option that can actually be paid, instead of
returning nothing.

Verified against crafted 402 responses covering Base USDC, a non-USDC Base
asset, USDC on another network, and a mixed list where the unsupported option
comes first, plus a live quote against a real endpoint that still returns
$0.015.

Three rounds, three real findings, and each one was a consequence of the
previous round's fix. Thanks for the persistence.

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.

1 participant