Skip to main content

botsrated (Python)

The BotsRated payment guard for x402 clients, plus a simpler pre-payment check.

  • Before your agent signs, it looks up facts BotsRated measured about the seller's host and compares them with the payment request on your machine.
  • While the payment is built, it adds an x402 payment-identifier retry key, only when the seller advertises that extension.
  • After, it keeps a local record (never the signature) and can read the blockchain, read-only, to see whether the payment settled once.

It lowers the chance of paying the wrong address or paying twice; it can't rule either out.

Status: 0.2.0 is not released yet. The latest version on PyPI is botsrated 0.1.6, which does not include the payment guard.

What this does and doesn't do. This package checks facts BotsRated measured about a seller before your agent signs, adds a retry key to the payment request, and reads the blockchain to see whether a payment already settled. It can't promise that a payment is right, that a seller will deliver, or that you'll never pay twice: a retry key only stops a second charge if the seller supports the x402 payment-identifier extension, and on-chain reads can be late or fail. You choose the rules that stop a payment, and you remain responsible for what your agent pays. Provided under the MIT license, "as is", without warranty.

Hosted checks. By default (hosted=True) this package calls BotsRated's free API (https://botsrated.com/v1/preflight) and sends it only the seller's host name (never the path, query, wallet, amount, nonce, payment header or request body). Those calls are covered by the BotsRated Terms (https://botsrated.com/terms) and Privacy Policy (https://botsrated.com/privacy), not by the MIT license. hosted=False turns hosted checks off: nothing is then sent to botsrated.com, and only the local checks, the retry key and the on-chain check (which goes only to the RPCs you set) run.

Install

pip install "botsrated[x402]"

Runtime dependencies: httpx>=0.27,<1 (and, with the x402 extra, x402[httpx,evm]>=2.25,<3). The guard only uses the hook and extension points of the x402 client you pass in.

Payment Guard: Start With Dry-Run

import httpx
from x402 import x402Client
from x402.http.clients.httpx import x402AsyncTransport
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact import ExactEvmScheme
from botsrated import PaymentGuard

client = x402Client()
client.register("eip155:*", ExactEvmScheme(signer=EthAccountSigner(account)))

# Recommended first setting: log what the guard sees, change nothing.
guard = PaymentGuard(dry_run=True, on_decision=lambda d: print(d.message))
guard.install(client)  # before/after/response hooks (+ the retry-key extension when not in dry-run)

http = httpx.AsyncClient(transport=x402AsyncTransport(client, httpx.AsyncHTTPTransport()))
paid_get = guard.wrap_async_get(http.get)  # tells the guard which URL is being paid
await paid_get("https://seller.example/api")

x402ClientSync works too (guard.install(sync_client), then guard.wrap_get(session.get) or with guard.request(url): ...).

Importing the guard (from botsrated import PaymentGuard or from botsrated.payment_guard import PaymentGuard) does not load the signing client: BotsRatedClient, which can sign BotsRated's own paid lookups, loads only when you use it.

Always tell the guard the URL being paid (wrap_async_get, wrap_get or with guard.request(url):). Without it the guard only knows the host the seller's 402 claims (resource.url), so it reports paid_url_unknown (a warning by default; POLICIES["strict"] stops the payment).

When the log looks right, drop dry_run. The zero-config setting (PaymentGuard()) is advice only: it reports facts and never stops a payment. To stop payments, choose a policy yourself:

from botsrated import POLICIES, PaymentGuard

guard = PaymentGuard(policy=POLICIES["strict"])  # your choice

A stopped payment raises the x402 abort error with text like:

Payment stopped by your payment policy (rule: payToMismatch). Fact: the payTo in this payment request is not among the payTo addresses BotsRated's probes recorded for seller.example on eip155:8453. Receipt: https://botsrated.com/receipt/. This is a measurement, not a finding about the seller. Dispute: https://botsrated.com/disputes

Rules And Defaults

Rule (PaymentGuardPolicy(...)) Reason code POLICIES["warn"] (default) POLICIES["strict"]
pay_to_mismatch: the payTo is not among the payTos BotsRated's probes recorded for the host payto_not_observed warn block
pay_to_invalid: the payTo is not a 20-byte hex address payto_invalid warn block
resource_host_mismatch: the 402's resource.url names another host than the URL being paid (checked locally) resource_host_mismatch warn block
paid_url_unknown: the URL being paid is unknown (no wrap_get / guard.request(url)), so the only host is the 402's own claim paid_url_unknown warn block
price_above_observed: same route, amount above the last amount BotsRated recorded (compared locally) price_above_observed warn warn
not_covered: BotsRated has no probe record for the host not_covered info info
stale: BotsRated's latest probe is older than 24 h data_stale info info
BotsRated unreachable (3 s timeout) botsrated_unreachable info (fail_open=True) info unless fail_open=False
After payment: the seller did not report a settled payment payment_state_unknown warn warn

Each rule takes "allow", "warn" or "block", e.g. dataclasses.replace(POLICIES["warn"], pay_to_mismatch="block"). A custom PaymentGuardPolicy(...) leaves paid_url_unknown at "warn" unless you set it: add paid_url_unknown="block" (as POLICIES["strict"] does) if an unknown paid URL should stop the payment. Every decision carries reasons (code, effect, rule, fact), facts, policy_rule, dispute_url and a message. Reason texts state facts only; seller text is never echoed except a parsed host name.

Options

Option Default Meaning
policy POLICIES["warn"] Which facts warn and which stop a payment
dry_run False Log decisions, stop nothing, add no retry key. Recommended first setting
hosted True False: nothing is sent to botsrated.com
fail_open True False: stop the payment when BotsRated can't be reached or the guard errors
retry_key True Add a payment-identifier id where the seller advertises it
onchain default RPCs RPC settings for payment_state (rpcs, extra_rpc, margin_seconds, expiry_tag, confirm_tag), or False
timeout / cache_ttl 3 s / 5 min Hosted lookup timeout and per-host cache
preflight hosted call Your own lookup (host) -> (found, body)
on_decision none Receives every decision (errors in it are ignored)

Fail-Open Or Fail-Closed

  • Fail-open (default): if botsrated.com is down or slow, or the guard itself hits an error, the payment goes ahead and the decision says botsrated_unreachable (or hook_error). Our outage never stops your agent, but during an outage you get no seller facts.
  • Fail-closed (fail_open=False): the same cases stop the payment. Use it only if an unchecked payment is worse for you than a missed call.
  • Local checks (resource_host_mismatch, payto_invalid) and the on-chain check never depend on botsrated.com.

What The Guard Sends, And To Whom

  • botsrated.com: GET /v1/preflight?url=<host name> with User-Agent: botsrated-python/<version>. Never the path, query, wallet, amount, nonce, tx hash, payment header or request body. No install id. Covered by the Terms and Privacy Policy. Turn it off with hosted=False.
  • Base RPCs (only when you call payment_state; nothing calls them automatically): JSON-RPC reads that include the payer address and the payment's nonce, sent straight from your machine to the RPCs, never to BotsRated. See the privacy note under "Did It Settle Once?".
  • No remote configuration, feature flags or auto-update: behavior changes only with a new package version.

Retry Key (payment-identifier)

If the seller's 402 advertises the x402 payment-identifier extension, the guard adds a random id (pay_ + 128 random bits) through x402's extension mechanism. Nothing else in the payload changes, and the seller's 402 is never modified. Ids are never reused across sellers, payTo addresses or routes. Inside with guard.operation(key): a retry of the same logical operation (same payTo, host and path) reuses the id, so a seller that honors the extension can answer a retry from its cache (or refuse a different payload with 409) instead of charging again.

A retry key only stops a second charge if the seller supports it. In BotsRated's own probes on Oct 24, 2026, about 2.2% of seller hosts (81 of 3,739 in our probe database) advertised payment-identifier in their 402, and 6 of them required it. Elsewhere the guard adds nothing.

Did It Settle Once? (Read-Only)

rec = guard.records()[-1]              # pay_to, amount, nonce, valid_before, host; never the signature
state = guard.payment_state(rec)       # or: await guard.apayment_state(rec). Reads the chain; never pays, signs or retries

There are three answers only:

  • settled: two RPCs on different hosts (or two of three, see below) show the EIP-3009 authorization used at Base's safe block tag, the chain id is 8453, and the transaction receipt has AuthorizationUsed(from, nonce) and Transfer(from, payTo, amount) emitted by the pinned USDC contract 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.
  • unknown (payment_state_unknown) with wait_until: not settled yet, the RPCs disagree or failed, the blocks they read are not yet past validBefore plus the margin (chain_not_past_expiry), or the record can't be checked. Do not sign a new payment for that purchase before wait_until (the signed authorization stays valid until its validBefore, plus a margin of at least 30 s).
  • expired_unsettled: validBefore plus the margin has passed on the clock and in the chain: both RPCs show the authorization unused at a block (Base's safe tag by default) whose timestamp is at or after validBefore plus the margin. USDC accepts an authorization in any block before validBefore, so "unused" only counts in a later block. With the default 60 s margin this is usually 1 to 3 minutes after expiry. Only then is a fresh payment free of a double-charge risk from the first one.

Settings (onchain={...}): margin_seconds (default 60, minimum 30) is the extra wait after validBefore. It is clamped, not rejected: a value below 30 (including negative values and -inf) is raised to 30, a value above 86,400 (1 day, including inf) is lowered to 1 day, and NaN or a non-number uses 60. expiry_tag="finalized" waits for a finalized block past expiry instead (more conservative, usually 20+ minutes).

The guard never retries on unknown. botsrated.com is never asked; a tx hash the seller reports is only a hint that is verified on chain.

Default RPCs (two operators; both must answer and agree):

RPC Operator
https://mainnet.base.org Coinbase (Base public endpoint; rate-limited, not for production load)
https://base-rpc.publicnode.com Allnodes (PublicNode)
PaymentGuard(onchain={"rpcs": ["https://your-rpc-1", "https://your-rpc-2"]})
# Optional third RPC of your own: then 2 of the 3 must answer and agree, and any RPC that sees the
# authorization used rules out expired_unsettled.
PaymentGuard(onchain={"extra_rpc": "https://your-own-rpc"})

At most 3 RPCs in total (rpcs plus extra_rpc): more is a configuration error (ValueError), raised when the guard is created and by check_payment_state, before any RPC is called.

Privacy of on-chain checks. Each payment_state() call sends the payer address, the payment's nonce and your IP address to every RPC it uses. With the defaults those are Coinbase (mainnet.base.org) and Allnodes (base-rpc.publicnode.com); anyone holding the nonce can match it to the on-chain transaction and the seller. Treat both operators as logging. Pass your own RPCs (rpcs, extra_rpc) if that matters to you. BotsRated never receives these reads.

What The Guard Can't Do (By Design)

  • It never signs, never sends or resends a payment, never builds a payment header and never imports a signer.
  • Every hook is wrapped so its return value is discarded. The after-payment and response hooks always return None, so they can never ask x402 to "recover" (which would sign a fresh payment). The pre-payment hook can only return an AbortResult. The payment-creation-failure hook is never registered.
  • These limits are enforced by tests in CI: tests/test_c1_no_repay.py (fuzzed hook contexts, a grep of the guard's source and built wheel for signing, payment-header and retry code, an import allow-list, and a real x402 httpx run where the seller fails after payment and exactly one payload is created).
  • Other hooks can still pay again. If your own code or another library registers an on_payment_response or on_payment_creation_failure hook on the same x402 client that returns a result with recovered=True, x402 signs and sends a fresh payment. The guard can't stop that, because it happens outside this package. Don't return recovered from your own hooks unless a new payment is what you want.

The Pre-Payment Guard (PrePaymentGuard)

The older, simpler guard (pre-payment check only). Since 0.2.0 it also sends only the host name and is advice-only on payTo by default; pass policy=STRICT_GUARD_POLICY (or GuardPolicy(pay_to_mismatch="block")) to stop those payments as before. Since 0.2.0 STRICT_GUARD_POLICY also sets require_request_url=True, so it stops a payment whose real request URL is unknown; use wrap_get or x402_hook_for(url) with it.

from x402 import x402ClientSync
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact import ExactEvmScheme
from x402.http.clients import x402_requests
from botsrated import STRICT_GUARD_POLICY, PrePaymentGuard

client = x402ClientSync()
client.register("eip155:*", ExactEvmScheme(signer=EthAccountSigner(account)))
guard = PrePaymentGuard(policy=STRICT_GUARD_POLICY)  # the policy is your choice
client.on_before_payment_creation(guard.x402_hook)

# Tell the guard which URL is actually being paid (since 0.1.5):
session = x402_requests(client)
paid_get = guard.wrap_get(session.get)       # or: with paying_request(url): session.get(url)
paid_get("https://seller.example/api")

The 402's resource.url is chosen by the seller, so on its own it only says which host the seller claims to be. When the guard knows the request URL (guard.wrap_get(...), guard.wrap_async_get(...), with paying_request(url):, guard.x402_hook_for(url), or guard.decide(..., request_url=url)), it looks up that URL's host and blocks the payment with resource_url_mismatch if resource.url names a different host. That block applies even with fail_open=True. Hosts are compared after IDNA (punycode) and trailing-dot normalization. The wrappers only see the URL you called, not where redirects ended, so a resource.url that differs only by a leading www. (an apex <-> www redirect) is accepted (since 0.1.6); any other host is blocked. Without a request URL the host is still read from resource.url (as before 0.1.5); set GuardPolicy(require_request_url=True) to block those payments instead.

Defaults:

  • A payTo that differs from every payTo BotsRated's own probes saw for the host, on the same network, is a warning (payto_not_observed; it was a block before 0.2.0). The seller's directory listing alone never confirms a payTo: with no probe observation, a listing match is noted with payto_listing_only and a listing mismatch follows the same rule.
  • A price above the last observed price on the same route is a warning.
  • A host that is not covered is allowed.
  • Data older than 24 h is allowed (data_stale).
  • If BotsRated cannot be reached (4 s timeout), the payment is allowed (fail_open, botsrated_unreachable).
  • The minimum valid-402 rate is off.

Change these with GuardPolicy(...). Preflight answers are cached for 5 minutes per host.

Opt-In Paid Lookups With A Budget

from botsrated import BotsRatedClient, PrePaymentGuard

botsrated = BotsRatedClient(budget_usd="0.05", x402_client=client)   # signs only the validated quote
guard = PrePaymentGuard(botsrated, paid_check_at_or_above_usd=0.25)
client.on_before_payment_creation(guard.x402_hook)

botsrated.check("seller.example")     # $0.001 (first 100/day free)
botsrated.report("seller.example")    # $0.002 (first 10/day free)
botsrated.best("web search")          # $0.01

x402_client= (since 0.1.4) is the recommended mode: the client validates the 402, rebuilds it from the allowlisted fields only (scheme, network, asset, amount, payTo, maxTimeoutSeconds, EIP-3009 name/version, resource url/description/mimeType; never extensions), signs that copy with client.create_payment_payload, and sends PAYMENT-SIGNATURE itself. The signer never sees the raw 402. The older paying_get=x402_requests(client).get still works, but that session re-reads the 402 itself, so register the guard on the same x402 client so the re-served 402 is checked again before signing.

How paid calls work:

  • Free daily quota: the first 100 /v1/check and 10 /v1/report calls per client per UTC day are served free (200 with charged: false and a free_quota block). The client asks unpaid first, so inside the quota nothing is paid and the budget is untouched; check()/report() also work without a budget until the quota is used up (then paid_disabled). client.last_free_quota shows what is left.
  • Otherwise the client reads the 402 quote unpaid first. It accepts the quote only if every option is exact USDC on Base to BotsRated's payTo 0x7F936f42F46B6c4Cbd3242cF8A1759dfe829E349, at or below the published price.
  • The amount is reserved from the budget before paying. If the paid call fails or raises, it stays counted (the payment may already have settled), so the budget is a hard ceiling.
  • Amounts must be ASCII digits only ([0-9]+); since 0.1.5 a quote with e.g. Arabic-Indic or full-width digits is refused.
  • Only a one-shot EIP-3009 payment is signed: an option whose extra holds anything beyond the token's name/version (e.g. assetTransferMethod: permit2, paymentFlow), a missing maxTimeoutSeconds or one above 600 (since 0.2.0 a missing value is invalid), or payment extensions in the 402 (e.g. erc20ApprovalGasSponsoring; only bazaar and payment-identifier are tolerated, and neither is signed) rejects the quote with terms_rejected before anything is paid.
  • With x402_client=, if signing fails nothing can settle and the reservation is released (payment_not_created).
  • When the guard is registered, it refuses any payment to botsrated.com that the client did not reserve or that carries payment extensions, and strips extensions before the x402 signer runs.

Tests

uv run --with pytest --with respx --with pytest-asyncio --with "x402[httpx,evm]" pytest -m "not live"

Security reports: SECURITY.md.

License: MIT. BotsRated reports measurements, not endorsements. BotsRated is independent and is not affiliated with or endorsed by Coinbase, the x402 Foundation or any seller. "x402" is used only to describe compatibility.

Metadata

Release files for botsrated 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for botsrated 0.2.0
File Size Uploaded Provenance Actions
botsrated-0.2.0.tar.gz 55.6 kB With provenance Details for botsrated-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for botsrated 0.2.0
File Interpreter ABI Platform Actions
botsrated-0.2.0-py3-none-any.whl Python 3 none any Details for botsrated-0.2.0-py3-none-any.whl

Total release size: 96.4 kB

Release files / botsrated-0.2.0.tar.gz

Download URL botsrated-0.2.0.tar.gz
Size 55.6 kB
Tags Source
SHA-256 checksum
How to use checksums
6f4bc21ae31b1acacf461a5f5d6702d73aadbd701d8cbe5c802a714e0941901f
BLAKE2b-256 checksum
How to use checksums
e147fed8bbc23c8e7277c0317db4ba5774cd272a02b1d3c656c9a1c6750efb5f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Release files / botsrated-0.2.0-py3-none-any.whl

Download URL botsrated-0.2.0-py3-none-any.whl
Size 40.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4b6eeb96cea5b413e71cb231a1069edf88787e8648947a3fe01fc0d2968e1616
BLAKE2b-256 checksum
How to use checksums
6aafb8c58e6c61a50a36187d0029949ab210b6d3536dac86fa54c032c2420460
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

Security for botsrated 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for botsrated-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page