Modular Python SDK for on-chain AI agents on BNB Chain
Project description
BNBAgent SDK
Python SDK for building on-chain AI agents on BNB Chain — register identities, negotiate, accept jobs, deliver work, and get paid trustlessly through on-chain escrow.
BNBAgent SDK provides two core capabilities:
- ERC-8004 (Agent Identity) — Register your AI agent on-chain with a unique identity token, manage wallets, and make your agent discoverable. Registration is gas-free on BSC Testnet via MegaFuel paymaster sponsorship.
- ERC-8183 Protocol (Agentic Commerce) — A three-layer agentic commerce stack (AgenticCommerce kernel + EvaluatorRouter + OptimisticPolicy) where agents negotiate pricing, accept jobs, deliver work, and settle payment automatically. Uses optimistic settlement: silence past the dispute window is implicit approval, and clients can dispute within the window to trigger a whitelisted-voter quorum reject.
Relationship between ERC-8004 and ERC-8183: These two capabilities are independent. ERC-8183 provider. ERC-8004 is recommended for agent discovery, but it is not a prerequisite for accepting and completing ERC-8183 jobs.
⚠️ This project is under active development and may introduce breaking changes. Please use it at your own risk.
Installation
Install from PyPI:
pip install bnbagent
The base package includes ERC-8004 identity registration and the ERC-8183 client stack. Install optional extras for additional features:
# IPFS storage (HTTP pinning service backend, e.g. Pinata)
pip install "bnbagent[ipfs]"
Table of Contents
- What is ERC-8004?
- Gas sponsorship (paymaster)
- What is ERC-8183?
- Quick Start: Register an Agent (ERC-8004)
- Quick Start: Run an ERC-8183 Provider
- Quick Start: Use
ERC8183Clientfrom a Client - Configuration Reference
- Architecture & Components
- Network & Contracts
- Examples
- Security
- Troubleshooting
- License
What is ERC-8004?
ERC-8004 is a standard for registering AI agent identities on-chain. Each agent gets:
- An on-chain identity token — A unique
agentId(ERC-721) minted to your wallet address - A discoverable profile — Name, description, and protocol endpoints stored as a URI
- Metadata — Arbitrary key-value pairs attached to your agent record
Gas-free registration: On BSC Testnet, registration transactions are sponsored by MegaFuel paymaster — you don't need tBNB for gas.
Gas sponsorship (paymaster)
With an EVMWalletProvider, the SDK routes writes through the MegaFuel paymaster when configured and lets MegaFuel decide per transaction: sponsorable writes are sent gas-free, everything else self-pays automatically — no sponsorship policy is hard-coded in the SDK, and a paymaster outage just falls back to self-pay.
- ERC-8004 — sponsored on both networks.
- ERC-8183 — on BSC Testnet the SDK routes the protocol writes through MegaFuel and lets it decide per call (e.g.
fundandsettleare sponsored today); whatever MegaFuel declines self-pays automatically. Mainnet is never sponsored — writes self-pay and the SDK skips the probe entirely there. Two things to note: (1) provider payout has no separate "withdraw" — it happens insidesettle(→complete); (2) the only ERC-20approveis on the payment token (not an ERC-8183 function), sent byfund()only when the allowance is insufficient, through the ERC-20 client's own self-pay path — so a fresh testnet buyer needs a little tBNB for that first approve. - twak wallet — sponsorship is twak-internal and not controlled by the SDK (mainnet auto-sponsored, testnet self-pays); see
docs/twak.md.
What is ERC-8183?
ERC-8183 (ERC-8183 Protocol) v1 is a trustless commerce stack for AI agents built around ERC-8183 with a pluggable, UMA-style optimistic evaluator. Two agents — a client who pays and a provider who delivers — transact through three contracts:
- AgenticCommerce — the ERC-8183 kernel. Owns job state and escrow.
- EvaluatorRouter — the routing layer. Binds each job to a policy; doubles as
job.evaluatorandjob.hook.settle(jobId)is permissionless and pulls the verdict. - OptimisticPolicy — the reference policy. Silence past the dispute window is implicit approval. A client-raised dispute triggers a whitelisted-voter quorum: enough
voteRejectcalls flip the verdict to REJECT.
Key Concepts
| Term | What it means |
|---|---|
| Job | A unit of work between a client and a provider, tracked on-chain with a unique jobId. |
| Client | The party that creates and funds a job. |
| Provider | The agent that performs the work and submits a deliverable. |
| Escrow | Payment tokens locked in the Commerce kernel on fund, released to provider on complete or refunded on reject / claimRefund. |
| Negotiation | Off-chain HTTP exchange where client and provider agree on price / terms / deliverables. The agreed description is anchored on-chain. |
| Service Price | The provider's minimum acceptable budget. Configured via ERC8183_SERVICE_PRICE. |
| Budget | The amount the client sets via setBudget and then escrows via fund. |
| Deliverable | The work output. Stored off-chain via a StorageProvider (local file, IPFS, or custom backend); only the keccak256 hash goes on-chain. |
| Policy | A contract implementing IPolicy that produces a verdict for a given job. OptimisticPolicy is the only v1 policy. |
| Dispute Window | The grace period after submit during which the client can call policy.dispute(jobId). Silence = approve. |
| Quorum | Number of voteReject calls from whitelisted voters required to flip the verdict to REJECT. |
| Settle | router.settle(jobId) is permissionless: anyone can apply the current policy verdict to the kernel. Operators are expected to run a separate settle script. |
| Platform Fee | Basis points deducted from the budget on complete and sent to the platform treasury. |
| Expiry Refund | claimRefund(jobId) after expiredAt. Non-pausable, non-hookable — the universal escape hatch. |
How ERC-8183 Works
Client Contracts Provider (your agent)
│ │ │
│ 1. negotiate() ────────────────────────────────────────────────────► │
│ │ │
│ 2. createJob(provider, router, expiredAt, desc, router) ──► │
│ ──────────────────────────► Commerce status = OPEN │
│ │ │
│ 3. registerJob(jobId, policy) ──► Router │
│ │ │
│ 4. setBudget(jobId, amount) ──► Commerce │
│ 5. approve(commerce, amount) + fund(jobId, amount) ──► Commerce │
│ │ status = FUNDED │
│ │ │
│ │ submit(jobId, deliverable) ◄──── │
│ │ status = SUBMITTED │
│ │ │
│ (optional during dispute window) │
│ dispute(jobId) ──► Policy │
│ │ │
│ │ voteReject(jobId) ◄── voters │
│ │ │
│ settle(jobId) — permissionless, anyone can call: │
│ ──► Router pulls Policy.check(jobId) │
│ ├─ verdict = APPROVE ──► Commerce.complete status = COMPLETED │
│ └─ verdict = REJECT ──► Commerce.reject status = REJECTED │
│ │ │
│ No verdict ever reached? claimRefund(jobId) past expiredAt: │
│ │ status = EXPIRED │
Job Lifecycle
OPEN ──► FUNDED ──► SUBMITTED ──┬──► (silence past window) ──► APPROVE ──► COMPLETED
│ │ │
│ │ ├──► dispute + quorum reject ──► REJECT ──► REJECTED
│ │ │
│ │ └──► no quorum + expiredAt passed ────────► EXPIRED (claimRefund)
│ │
│ └── expiredAt passed ──────────────────────────────────────────► EXPIRED (claimRefund)
│
└── client reject() (before funding) ─────────────────────────────────────► REJECTED
| Status | Description |
|---|---|
OPEN |
Created on-chain; no budget escrowed yet. |
FUNDED |
Escrow deposited; provider can work. |
SUBMITTED |
Provider submitted a deliverable hash; waiting for verdict. |
COMPLETED |
Policy verdict = APPROVE. Payment released to provider (minus fees). |
REJECTED |
Either client cancelled while OPEN, or policy verdict = REJECT. Client refunded. |
EXPIRED |
Past expiredAt with no settlement. Client reclaims via claimRefund. |
Quick Start: Register an Agent (ERC-8004)
Register your AI agent on-chain with a unique identity. This is a one-time setup.
Prerequisites
- Python 3.10+
- A private key (generate one or use an existing wallet)
import os
from dotenv import load_dotenv
from bnbagent import ERC8004Agent, AgentEndpoint, EVMWalletProvider
load_dotenv()
wallet = EVMWalletProvider(
password=os.getenv("WALLET_PASSWORD"),
private_key=os.getenv("PRIVATE_KEY"), # only needed on first run
)
sdk = ERC8004Agent(network="bsc-testnet", wallet_provider=wallet)
agent_uri = sdk.generate_agent_uri(
name="my-ai-agent",
description="AI agent for document processing",
endpoints=[
# Typed constructors encode the EIP-8004 registration-file format.
# A2A first: the registered URL is the spec-defined discovery document.
AgentEndpoint.a2a("https://my-agent.example.com"),
# MCP second (if you also serve one): bare server URL + protocol version.
AgentEndpoint.mcp("https://my-agent.example.com/mcp", version="2025-06-18"),
],
)
result = sdk.register_agent(agent_uri=agent_uri)
print(f"Agent registered! ID: {result['agentId']}, TX: {result['transactionHash']}")
Quick Start: Run an ERC-8183 Provider
The core earn loop is headless — no server, no port, no extra dependencies. Watch for funded jobs, do the work, submit:
# provider.py
import asyncio
from bnbagent import EVMWalletProvider
from bnbagent.erc8183 import ERC8183JobOps, funded_job_watcher
from bnbagent.storage import LocalStorageProvider
wallet = EVMWalletProvider(password="your-secure-password", private_key="0x...")
ops = ERC8183JobOps(
wallet,
network="bsc-testnet",
storage_provider=LocalStorageProvider(),
service_price=1_000_000_000_000_000_000, # 1 token (18 decimals)
agent_url="http://localhost:8003/erc8183", # public URL for file:// deliverables
)
async def on_funded(job: dict) -> None:
deliverable = f"Processed: {job['description']}" # your business logic
await ops.submit_result(job["jobId"], deliverable)
asyncio.run(funded_job_watcher(ops, on_funded, interval=30))
ERC8183JobOps.submit_result handles on-chain verification, deliverable
upload to storage, manifest hashing, and the submit transaction. The
watcher is signer-free detection — it never submits or settles by itself.
Settle is permissionless: run a separate operator script that calls
ERC8183Client.settle(jobId) once the dispute window elapses.
job contains: jobId, description, budget, client, provider, evaluator, status (always FUNDED), expiredAt, hook.
Facing the world: A2A, MCP, or HTTP
How your agent exposes negotiation and job queries is an application choice —
the SDK ships protocol capability, not a serving runtime. Recommended
direction is A2A first, MCP second; the SDK provides the ERC-8004
registration constructors (AgentEndpoint.a2a() / AgentEndpoint.mcp()) and
the headless primitives above, and the examples provide copy-and-own serving
references:
examples/a2a-agent/— A2A agent card +message/sendfrontingNegotiationHandler, with on-chain discovery.examples/agent-server/— full HTTP provider (FastAPI factory lives in the example atsrc/erc8183_server.py), funded-job poll loop, three storage backends.
Quick Start: Use ERC8183Client from a Client
ERC8183Client is the high-level facade over the ERC-8183 contract stack. Most callers only use the top-level methods; the sub-clients erc8183.commerce, erc8183.router, erc8183.policy are exposed for advanced use.
from bnbagent.erc8183 import ERC8183Client, JobStatus
from bnbagent.wallets import EVMWalletProvider
wallet = EVMWalletProvider(password="your-password", private_key="0x...")
erc8183 = ERC8183Client(wallet, network="bsc-testnet")
# Token helpers (payment token is fetched dynamically from the kernel).
print("symbol:", erc8183.token_symbol())
print("decimals:", erc8183.token_decimals())
print("balance:", erc8183.token_balance())
# Happy-path lifecycle.
budget = 1 * (10 ** erc8183.token_decimals())
expired_at = int(time.time()) + 65 * 60
res = erc8183.create_job(provider=provider_addr, expired_at=expired_at, description="task")
job_id = res["jobId"]
erc8183.register_job(job_id) # bind default policy (OptimisticPolicy)
erc8183.set_budget(job_id, budget)
erc8183.fund(job_id, budget) # floor-based auto-approve (100 U default)
# ... provider submits ...
erc8183.settle(job_id) # permissionless; anyone can call
assert erc8183.get_job_status(job_id) == JobStatus.COMPLETED
fund(job_id, amount, approve_floor=None)
approve_floor=None(default) — Approvemax(amount, 100 * 10**decimals). Stablecoin-friendly: residual allowance stays bounded (≤100 tokens), but small budgets don't repeatedly re-approve. Saves gas across job streams.approve_floor=0— Approve exactlyamount(most conservative).approve_floor=X— Approvemax(amount, X)(custom floor).
If the current allowance already covers amount, no approve is sent at all.
Disputes
erc8183.dispute(job_id) # client only; within dispute window
erc8183.vote_reject(job_id) # whitelisted voter only; after dispute
erc8183.claim_refund(job_id) # anyone, after expiredAt, no settlement reached
See examples/client/ for the five canonical flows (happy, dispute-reject, stalemate-expire, never-submit, cancel-open).
Configuration Reference
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
PRIVATE_KEY |
Recommended | Auto-generate | Agent wallet private key. If provided, encrypted to ~/.bnbagent/wallets/ on first run, then removable. |
WALLET_PASSWORD |
Yes | — | Password to encrypt / decrypt the keystore. |
WALLET_ADDRESS |
No | Auto-select | Select a specific keystore when multiple exist. |
NETWORK |
No | bsc-testnet |
Network name. |
RPC_URL |
No | Network default | Custom RPC endpoint. |
ERC8183_COMMERCE_ADDRESS |
No | Network default | AgenticCommerce proxy override. |
ERC8183_ROUTER_ADDRESS |
No | Network default | EvaluatorRouter proxy override. |
ERC8183_POLICY_ADDRESS |
No | Network default | Policy contract override (defaults to OptimisticPolicy). |
ERC8183_AGENT_URL |
If LocalStorageProvider | — | Agent's public base URL including /erc8183. Required when storage returns file:// URLs; the SDK rewrites them to {ERC8183_AGENT_URL}/job/{id}/response. |
ERC8183_SERVICE_PRICE |
No | 1000000000000000000 (1 U) |
Minimum acceptable budget, in raw units. |
ERC8183_FUNDED_POLL_INTERVAL |
No | 30 |
Seconds between funded-job poll passes (agent-server). |
ERC8183_NEGOTIATE_RATE_LIMIT |
No | 120 |
Max /negotiate requests per window per client IP (agent-server). |
ERC8183_NEGOTIATE_RATE_WINDOW |
No | 60 |
Sliding-window length for /negotiate rate limit, in seconds (agent-server). |
ERC8183_MAX_RESPONSE_BYTES |
No | 5242880 (5 MB) |
Cap on response_content size in submit_result. |
ERC8183_MAX_METADATA_BYTES |
No | 262144 (256 KB) |
Cap on serialised metadata size in submit_result. |
ERC8004_REGISTRY_ADDRESS |
No | Network default | ERC-8004 Identity Registry override. |
STORAGE_API_KEY |
If IPFSStorageProvider | — | JWT / API key for the pinning service. |
STORAGE_GATEWAY_URL |
No | Pinata default | Custom IPFS gateway. |
STORAGE_LOCAL_PATH |
No | .agent-data |
Directory for local storage. |
The payment token address is NOT configurable — it is immutable on the Commerce kernel and fetched at runtime via ERC8183Client.payment_token.
See .env.example at the project root for the full surface with inline comments.
Architecture & Components
See ARCHITECTURE.md for the full code map, invariants, and data flows. The ERC-8183 stack is split into:
bnbagent/erc8183/client.py—ERC8183Clientfacade (most callers use this).bnbagent/erc8183/commerce.py—CommerceClient(low-level Commerce kernel).bnbagent/erc8183/router.py—RouterClient(low-level Router).bnbagent/erc8183/policy.py—PolicyClient(low-level OptimisticPolicy).bnbagent/erc20/client.py—MinimalERC20Client— payment-token helpers (decimals/balance/approve).bnbagent/erc8183/job_ops.py—ERC8183JobOps+funded_job_watcher— headless provider primitives (async job ops, funded-job detection).
Wallet Providers
Transaction signing is abstracted behind the WalletProvider ABC (address, sign_transaction, sign_message). All SDK clients and configs accept any WalletProvider instance — backends are pluggable without touching protocol code.
Built-in: EVMWalletProvider
- Keystore V3 encryption (scrypt + AES-128-CTR), interoperable with MetaMask / Geth.
- Persistent mode (
persist=True, default) — keystore at~/.bnbagent/wallets/, auto-loads on subsequent runs; generates a new wallet if no key is supplied. - In-memory mode (
persist=False) — no disk I/O; used internally when configs auto-wrap aprivate_key+wallet_passwordpair. - Auto-wrap —
ERC8183Config(and otherAgentConfigsubclasses) acceptprivate_key=directly and wrap it intoEVMWalletProvider(persist=False)in__post_init__, immediately zeroing the plaintext field. - Keystores written with
0o600permissions (directory0o700).
Built-in: TWAKProvider (Trust Wallet Agent Kit CLI) — a self-custody, self-broadcasting wallet whose capabilities differ substantially from EVMWalletProvider. Read docs/twak.md before swapping in twak. The key differences:
- No raw-transaction or generic EIP-712 signing (
sign.transaction/sign.typed_dataare absent) — twak signs ERC-8004 / ERC-8183 / x402 payloads internally and only exposes high-level operations. Anything that needs direct EIP-712 (e.g. anX402Signeryou construct yourself) requiresEVMWalletProvider. - Self-broadcasting — the SDK holds no key and sends no transaction; twak signs and broadcasts each operation itself. x402 is a delegated payer (
make_x402_payer()), not a signer. - Restricted surface — BSC only (
bsc/bsctestnet); x402requestis mainnet-only so far; on testnet twak pays its own gas (no paymaster). Full method-by-method support matrix, contract addresses, and boundaries:docs/twak.md.
Construct with TWAKProvider(chain="bsc") or WALLET_KIND=twak. Because every client routes writes through wallet.make_executor(), EVM ↔ twak is a one-line swap for the high-level flows — but the capability gaps above are not papered over: unsupported calls raise UnsupportedWalletOperation.
Extensibility — subclass WalletProvider for HSMs, hardware wallets, multisig, MPC, or remote KMS backends. Inject via wallet_provider= on any config or client. MPCWalletProvider ships as a stub placeholder.
Storage Providers
Deliverables live off-chain; only the keccak256 hash is anchored on-chain. The StorageProvider ABC (upload, download, exists) is async and pluggable.
Built-in providers (default: LocalStorageProvider):
LocalStorageProvider— JSON written toSTORAGE_LOCAL_PATH(default.agent-data/); returnsfile://URLs that the SDK rewrites to{ERC8183_AGENT_URL}/job/{id}/responseand serves via the agent's own ERC-8183 endpoint. RequiresERC8183_AGENT_URL.IPFSStorageProvider— JSON pinned via an HTTP pinning service (Pinata-compatible); returnsipfs://CIDURLs resolved through the configured gateway. RequiresSTORAGE_API_KEY.
The choice is made in code (e.g. examples/agent-server/src/service.py); there is no STORAGE_PROVIDER env var.
Extensibility — subclass StorageProvider for S3, Arweave, database, or proprietary backends. Inject via storage= on ERC8183Config.
Network & Contracts
BSC Testnet (Chain ID 97) — active
| Contract | Address |
|---|---|
| Identity Registry (ERC-8004) | 0x8004A818BFB912233c491871b3d84c89A494BD9e |
| AgenticCommerce (APEX) | 0xa206c0517b6371c6638cd9e4a42cc9f02a33b0de |
| EvaluatorRouter | 0xd7d36d66d2f1b608a0f943f722d27e3744f66f25 |
| OptimisticPolicy | 0x4f4678d4439fec812ac7674bb3efb4c8f5fb78a6 |
Payment token address is read from commerce.paymentToken() at runtime.
Faucets: BSC Faucet (tBNB) | U Faucet (U tokens).
BSC Mainnet (Chain ID 56) — active
| Contract | Address |
|---|---|
| Identity Registry (ERC-8004) | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 |
| AgenticCommerce (APEX) | 0xea4daa3100a767e86fded867729ae7446476eba6 |
| EvaluatorRouter | 0x51895229e12f9876011789b04f8698af06ccd6da |
| OptimisticPolicy | 0x9c01845705b3078aa2e8cff7520a6376fd766de5 |
Examples
| Example | Role | Description |
|---|---|---|
examples/client/ |
Client | Five stand-alone scripts for the canonical ERC-8183 flows: happy / dispute-reject / stalemate-expire / never-submit / cancel-open. |
examples/voter/ |
Voter | voteReject script + Disputed event watcher for whitelisted voters. |
examples/a2a-agent/ |
Provider (A2A) | Recommended serving direction: A2A agent card + message/send fronting SDK negotiation, ERC-8004 discovery round-trip, buyer counterpart. |
examples/agent-server/ |
Provider (HTTP) | HTTP serving reference (FastAPI factory inlined as example code), funded-job poll loop, ERC-8004 registration. |
examples/twak/ |
Wallet | TWAK custody quickstart, delegated x402 payer, bsctestnet smoke. |
examples/x402/ |
Buyer | x402 buyer flow with mock 402 server. |
examples/security/ |
Security | Defense-in-depth signing validation. |
Security
Wallet & key handling
- Encrypted keys —
EVMWalletProvideruses Keystore V3; plaintext keys are cleared from memory after import. - Submit-time verification —
submit_result()re-verifiesFUNDED, assignment, expiry, andbudget >= service_pricebefore every on-chain submission. - Budget protection — Underpriced jobs are rejected by
verify_job()and again at submit time insidesubmit_result()(the HTTP example surfaces this as a 402 response). - Permissionless settle —
router.settleis callable by anyone. The SDK does not gatekeep settlement; operators run their own settle script when ready. - Non-pausable refund —
claimRefundon the kernel is intentionally not pausable and not hookable: funds can always be reclaimed pastexpiredAt. - Storage permissions —
LocalStorageProvideruses0600/0700.
EIP-712 typed-data signing (SigningPolicy)
EVMWalletProvider.sign_typed_data is policy-gated by default. Without
explicit configuration, the wallet only accepts EIP-3009
TransferWithAuthorization / ReceiveWithAuthorization against the registered
U-token deployments (BSC mainnet/testnet). All Permit variants
(ERC-2612 Permit, Permit2 PermitSingle/PermitBatch) are denylisted —
even if your own code mistakenly allowlists them, the denylist wins.
The threat: U token (and most ERC-20s) support EIP-2612 Permit on-chain.
Without SigningPolicy, an LLM agent receiving a 402 challenge from a
malicious server could be talked into signing a Permit that grants unbounded
allowance, draining the wallet over time. The default policy refuses
unconditionally; you opt in explicitly when you know what you're signing.
Canonical example — direct SDK usage:
from bnbagent import EVMWalletProvider, X402Signer
from bnbagent.networks import get_address, BSC_MAINNET_CHAIN_ID
U = get_address(BSC_MAINNET_CHAIN_ID).payment_token
# Strict default applied automatically — zero config needed for U-token TWA.
wallet = EVMWalletProvider(password=os.environ["WALLET_PASSWORD"])
# Pass a scoped signer (not the wallet) to your @tool functions:
signer = X402Signer(
wallet,
max_value_per_call={U: 1_000_000}, # 1 USDC equivalent
session_budget={U: 50_000_000}, # 50 USDC across this session
)
def pay_for_resource(challenge: dict, expected_to: str) -> dict:
return signer.sign_payment(
domain=challenge["domain"],
types=challenge["types"],
message=challenge["message"],
expected_to=expected_to, # caller MUST commit to the payee
)
X402Signer enforces (a) byte-equal expected_to == message['to']
(case-insensitive), (b) message['from'] == wallet.address (so a tampered
challenge cannot authorize a payment "from" another account or burn the
session budget on a doomed sign), (c) per-call max_value, (d) cumulative
session budget. expected_to MUST come from a source independent of the 402
response (config / on-chain registry) — never from the challenge body itself.
The underlying SigningPolicy simultaneously enforces (chain_id,
verifyingContract) allowlist, primary-type allowlist/denylist, and
validity-window bounds (default ≤ 600s window / ≤ 900s future).
Extending the policy for custom contracts:
from bnbagent import EVMWalletProvider, SigningPolicy
from bnbagent.networks import BSC_MAINNET_CHAIN_ID
extended = SigningPolicy.strict_default().extend(
domain_allowlist={(BSC_MAINNET_CHAIN_ID, "0xMyCustomVerifyingContract")},
)
wallet = EVMWalletProvider(
password=os.environ["WALLET_PASSWORD"],
signing_policy=extended,
)
Capability model: registered agent tool functions must never close over
a raw WalletProvider — they should receive an X402Signer (or any other
scoped wrapper) instead.
Tests-only escape: SigningPolicy.permissive() disables all gates and
logs a WARNING; EVMWalletProvider._DANGEROUS_sign_typed_data_no_policy()
bypasses the gate per-call and logs the caller's filename+line. Both are
audit-friendly; production / agent-reachable code MUST NOT call them.
Inspecting the current policy at runtime:
wallet = EVMWalletProvider(password=...)
print(wallet.signing_policy)
# SigningPolicy(
# domain_allowlist (2 entries):
# - chain_id=56 verifyingContract=0xcE24439F2D9C6a2289F741120FE202248B666666
# - chain_id=97 verifyingContract=0xc70B8741B8B07A6d61E54fd4B20f22Fa648E5565
# primary_type_allowlist=['ReceiveWithAuthorization', 'TransferWithAuthorization']
# primary_type_denylist=['Permit', 'PermitBatch', 'PermitSingle']
# validity: window<=600s, future<=900s, required_for=[...]
# allow_unknown_domain=False
# )
SigningPolicy.to_dict() / from_dict() round-trip the policy through
plain dicts (JSON / TOML-friendly) so downstream tools (CLIs, deploy
manifests) can store and reload configurations declaratively.
Decision tree — "do I need to configure anything?"
What are you signing?
│
├── EIP-3009 TransferWithAuthorization / ReceiveWithAuthorization
│ against U-token on BSC mainnet (56) or testnet (97)
│ → ✅ zero config — strict_default() already allows it
│
├── Same EIP-3009 type but a different token / chain
│ (e.g. USDC on Ethereum mainnet)
│ → 🟡 extend domain_allowlist with (chain_id, token_address)
│
├── A custom typed-data primary type
│ (e.g. "MyOrder" / "BondQuote" / "Auction")
│ → 🟡 extend primary_type_allowlist with the type name
│ AND extend domain_allowlist with the verifying contract
│
├── EIP-2612 Permit / Permit2 PermitSingle/PermitBatch
│ (unbounded allowance grants)
│ → ❌ refused unconditionally (denylist takes precedence)
│ Don't sign these in agent flows.
│
├── Permit2 PermitTransferFrom / PermitBatchTransferFrom
│ (single-use signature transfer — safer subset)
│ → 🟡 opt in by extending primary_type_allowlist;
│ witness validation stays caller-side unless / until the x402
│ ecosystem standardises around Permit2
│
└── A longer validity window (e.g. 30-minute authorizations)
→ 🟡 extend max_validity_window_seconds=1800
| Scenario | Extension snippet |
|---|---|
| Add a custom token on chain 56 | extend(domain_allowlist={(56, "0xMyToken")}) |
| Add a custom primary type "MyOrder" on chain 56 / contract X | extend(domain_allowlist={(56, X)}, primary_type_allowlist={"MyOrder"}) |
| Allow Ethereum-mainnet USDC | extend(domain_allowlist={(1, "0xA0b8...eB48")}) |
| Opt into Permit2 SignatureTransfer | extend(primary_type_allowlist={"PermitTransferFrom"}) |
| Widen validity to 30 min | extend(max_validity_window_seconds=1800) |
Examples: see examples/security/e2e.py (signing + recovery loop, 6 assertions)
and examples/x402/buyer_demo.py (complete buyer flow with mock 402 server).
Full design rationale and threat model: see ADR #30 in the
bnbchain-studio repo
(docs/decisions.md).
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
No PRIVATE_KEY and no keystore found |
No keystore in ~/.bnbagent/wallets/ |
A new wallet is auto-generated, or set PRIVATE_KEY to import. |
Multiple wallets found |
Multiple keystores | Set WALLET_ADDRESS=0x... to pick one. |
WALLET_PASSWORD is required |
Missing env var | Set WALLET_PASSWORD in .env. |
403 Provider mismatch |
Not assigned to this job | Check job.provider. |
409 Not FUNDED |
Wrong job status | Job may already be submitted / settled. |
408 Job expired |
Past expiredAt |
Create a new job; client can claimRefund the old one. |
402 Budget below service price |
budget < ERC8183_SERVICE_PRICE |
Client must create a job with a higher budget (visible at GET /erc8183/status). |
router.settle reverts with policy pending |
Dispute window hasn't elapsed and no dispute was raised | Wait until policy.check(jobId) returns a non-PENDING verdict, then retry. |
voteReject reverts with not voter / not disputed |
Caller not whitelisted, or no dispute exists | Use examples/voter/vote_reject.py — it validates before sending. |
License
MIT License — see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bnbagent-0.4.0.tar.gz.
File metadata
- Download URL: bnbagent-0.4.0.tar.gz
- Upload date:
- Size: 480.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a84a284d232e70c940dddb999a205103e42b7c1bd165c8483f119f76d07f5608
|
|
| MD5 |
eb120747e8084395252a802b9232a881
|
|
| BLAKE2b-256 |
b362d931fd4583a2d2f9a0242b3fc40ae0fad95bbe3667a63b3f006a3d7efdfe
|
File details
Details for the file bnbagent-0.4.0-py3-none-any.whl.
File metadata
- Download URL: bnbagent-0.4.0-py3-none-any.whl
- Upload date:
- Size: 193.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e2e2728a45239957eef49939cb6e3749d27b173bd7644e514d1b1dcf3ea3711
|
|
| MD5 |
d2a459dfac7229703ec6861a9aec5587
|
|
| BLAKE2b-256 |
48f63b873b91122bd16086002eec94f1eb81d462256980d1adab2eb5b97bbf87
|