BNBAgent SDK
Python SDK (pip install bnbagent, Python 3.10+) for on-chain AI agents on BNB Chain. Two independent capabilities:
- ERC-8004 (identity) — register an agent on-chain as an ERC-721 identity token with a discoverable profile URI. Gas-free on BSC Testnet via MegaFuel paymaster.
- ERC-8183 (agentic commerce) — trustless job escrow between a client (pays) and a provider (delivers). Optimistic settlement: silence past the dispute window = approval; a client dispute triggers a whitelisted-voter quorum reject.
They are independent: you can run ERC-8183 jobs without ERC-8004 registration (registration is only recommended for discovery).
Active development — breaking changes possible. Tested with
bnbagent==0.4.0; verify install withpython -c "import bnbagent; print(bnbagent.__version__)". Optional extra:pip install "bnbagent[ipfs]"for IPFS/Pinata deliverable storage.
⚠️ Mainnet economics — read BEFORE any mainnet commerce
- The payment token is U (United Stables)
0xcE24439F2D9C6a2289F741120FE202248B666666— not BNB, not USDT/USDC. There is no faucet for it: acquire U on PancakeSwap (a WBNB–U pair exists). The client must hold U before callingfund(). Fetch decimals at runtime viaerc8183.token_decimals()— don't assume. - ERC-8183 writes on mainnet are never gas-sponsored. Client and provider both need BNB for gas (only ERC-8004 identity registration is sponsored on mainnet).
- The mainnet dispute window is 604800s (7 days). A happy path cannot reach
COMPLETEDin one session — silence-approval only kicks in after the window. Plan a partial E2E (throughSUBMITTED) and settle later via a cron/operator script (examples/auto_settle.py).settlereverting withpolicy pendingduring this week is expected, not an error.
Preflight balance checklist (mainnet)
| Party | Needs | Why |
|---|---|---|
| Client | BNB | gas for createJob/registerJob/setBudget/approve/fund (+ optional swap gas) |
| Client | U | the job budget escrowed by fund (+ small residual for retries) |
| Provider | BNB | gas for submit — not sponsored on mainnet |
| Settler (anyone) | BNB | gas for settle after the window |
Decide what you're doing
| Goal | Use | Reference |
|---|---|---|
| Register agent identity on-chain | ERC8004Agent | quick start below |
| Earn: accept + deliver funded jobs | ERC8183JobOps + funded_job_watcher | quick start below, examples/agent-server/, examples/a2a-agent/ |
| Pay: create/fund/settle jobs | ERC8183Client | quick start below, examples/client/ |
| Vote on disputes (whitelisted voter) | PolicyClient.vote_reject | examples/voter/ |
| Pay HTTP 402 challenges (x402) | X402Signer / twak delegated payer | examples/x402/, references/twak.md |
| Understand internals / extend | — | references/architecture.md |
| Wallet backends (EVM keystore vs twak) | EVMWalletProvider / TWAKProvider | references/wallets.md, references/twak.md |
Quick start: register an agent (ERC-8004)
One-time setup. Needs a private key (auto-generated if omitted) and WALLET_PASSWORD.
import os
from bnbagent import ERC8004Agent, AgentEndpoint, EVMWalletProvider
wallet = EVMWalletProvider(
password=os.getenv("WALLET_PASSWORD"),
private_key=os.getenv("PRIVATE_KEY"), # only needed on first run; keystore persists to ~/.bnbagent/wallets/
)
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=[
AgentEndpoint.a2a("https://my-agent.example.com"), # A2A first (discovery doc URL)
AgentEndpoint.mcp("https://my-agent.example.com/mcp", version="2025-06-18"), # MCP second, if served
],
)
result = sdk.register_agent(agent_uri=agent_uri)
# result["agentId"], result["transactionHash"]
Quick start: provider (earn loop, headless)
No server needed. Watch for funded jobs, do the work, submit:
import asyncio
from bnbagent import EVMWalletProvider
from bnbagent.erc8183 import ERC8183JobOps, funded_job_watcher
from bnbagent.storage import LocalStorageProvider
wallet = EVMWalletProvider(password="...", private_key="0x...")
ops = ERC8183JobOps(
wallet,
network="bsc-testnet",
storage_provider=LocalStorageProvider(),
service_price=1_000_000_000_000_000_000, # min acceptable budget, raw units (18 decimals here)
agent_url="http://localhost:8003/erc8183", # public URL; required for file:// deliverable rewriting
)
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))
submit_resulthandles verification (FUNDED status, assignment, expiry, budget ≥ service_price), deliverable upload, manifest hashing, and thesubmittx.- The watcher never submits or settles by itself. Settlement is a separate step — run an operator script calling
ERC8183Client.settle(job_id)after the dispute window elapses (examples/auto_settle.py,examples/agent-server/scripts/settle.py). jobdict fields:jobId,description,budget,client,provider,evaluator,status(alwaysFUNDED),expiredAt,hook.- Serving surface (A2A/MCP/HTTP) is your choice — copy-and-own references in
examples/a2a-agent/(recommended) andexamples/agent-server/(FastAPI). - Deliverable storage: placeholder URLs like
https://example.invalid/manifest.json(as inexamples/client/happy.py) are chain-only demos — voters cannot verify the deliverable. For anything a client might dispute, use real storage (LocalStorageProviderbehind a publicERC8183_AGENT_URL, orIPFSStorageProvider).
Quick start: client (create and pay for a job)
import time
from bnbagent.erc8183 import ERC8183Client, JobStatus
from bnbagent.wallets import EVMWalletProvider
wallet = EVMWalletProvider(password="...", private_key="0x...")
erc8183 = ERC8183Client(wallet, network="bsc-testnet")
budget = 1 * (10 ** erc8183.token_decimals()) # DEMO-SCALE (1 full token). On MAINNET use tiny
# budgets, e.g. (10 ** dec) // 100 for 0.01 U.
expired_at = int(time.time()) + 65 * 60
job_id = erc8183.create_job(provider=provider_addr, expired_at=expired_at, description="task")["jobId"]
erc8183.register_job(job_id) # bind default policy (OptimisticPolicy)
erc8183.set_budget(job_id, budget)
erc8183.fund(job_id, budget) # escrows; auto-approves payment token with 100-token floor
# ... provider submits ... dispute window elapses ...
erc8183.settle(job_id) # permissionless — anyone can call
assert erc8183.get_job_status(job_id) == JobStatus.COMPLETED
Disputes and escape hatch:
erc8183.dispute(job_id) # client only, within dispute window after submit
erc8183.vote_reject(job_id) # whitelisted voters only, after dispute; quorum flips verdict to REJECT
erc8183.claim_refund(job_id) # anyone, after expiredAt if never settled — non-pausable escape hatch
fund(job_id, amount, approve_floor=None): default approves max(amount, 100 * 10**decimals) to avoid re-approving across job streams; approve_floor=0 approves exactly amount; no approve is sent if allowance already covers it.
Job lifecycle
OPEN ──► FUNDED ──► SUBMITTED ──┬─ silence past window ──► COMPLETED (provider paid, minus platform fee)
│ │ ├─ dispute + quorum reject ──► REJECTED (client refunded)
│ │ └─ no verdict + past expiredAt ──► EXPIRED (client claimRefund)
│ └─ past expiredAt ──► EXPIRED (claimRefund)
└─ client reject() before funding ──► REJECTED
Gas sponsorship matrix
| Protocol / write | BSC Testnet | BSC Mainnet |
|---|---|---|
ERC-8004 register_agent | ✅ sponsored (MegaFuel) | ✅ sponsored (MegaFuel) — works with a zero-BNB wallet |
| ERC-8183 create/fund/submit/settle | 🟡 per-call: MegaFuel decides (fund/settle sponsored today); declined calls self-pay | ❌ never sponsored — all writes self-pay BNB |
ERC-20 approve of payment token (sent inside fund when allowance short) | ❌ always self-pays — fresh testnet buyer needs a little tBNB | ❌ self-pays |
| twak wallet ops | ❌ twak self-pays (twak-internal, SDK has no control) | ✅ twak auto-sponsors — see references/twak.md |
Networks & contracts
BSC Testnet (chain 97) — faucets: tBNB, U tokens
| Contract | Address |
|---|---|
| Identity Registry (ERC-8004) | 0x8004A818BFB912233c491871b3d84c89A494BD9e |
| AgenticCommerce | 0xa206c0517b6371c6638cd9e4a42cc9f02a33b0de |
| EvaluatorRouter | 0xd7d36d66d2f1b608a0f943f722d27e3744f66f25 |
| OptimisticPolicy | 0x4f4678d4439fec812ac7674bb3efb4c8f5fb78a6 |
BSC Mainnet (chain 56)
| Contract | Address |
|---|---|
| Identity Registry (ERC-8004) | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 |
| AgenticCommerce | 0xea4daa3100a767e86fded867729ae7446476eba6 |
| EvaluatorRouter | 0x51895229e12f9876011789b04f8698af06ccd6da |
| OptimisticPolicy | 0x9c01845705b3078aa2e8cff7520a6376fd766de5 |
The payment token address is NOT configurable — it is read from the Commerce kernel at runtime (ERC8183Client.payment_token). On mainnet it resolves to U (United Stables) 0xcE24…6666 (see the mainnet economics section at the top).
Notes:
- The SDK constructor defaults to
network="bsc-testnet"— always passnetwork="bsc-mainnet"explicitly (orNETWORK=bsc-mainnet) for mainnet work, and printbscscan.com(nottestnet.bscscan.com) explorer links; some example scripts hardcode testnet URLs. - Discovery/indexer lag: the registry index may show a generic name (e.g.
Agent #198565) even when your URI carries the real name. The on-chain URI is the source of truth; indexer names can lag or stay generic.
Environment variables
Full annotated reference: references/env.example. The essentials:
| Variable | Required | Notes |
|---|---|---|
WALLET_PASSWORD | Yes | Encrypts/decrypts the keystore at ~/.bnbagent/wallets/. |
PRIVATE_KEY | First run only | Imported and encrypted, then removable. Auto-generates a wallet if absent. |
WALLET_ADDRESS | No | Pick a keystore when several exist. |
NETWORK | No (bsc-testnet) | Or bsc-mainnet. |
RPC_URL | No | Custom RPC endpoint. |
ERC8183_SERVICE_PRICE | No (1e18) | Provider's minimum budget, raw units. |
ERC8183_AGENT_URL | If LocalStorageProvider | Public base URL incl. /erc8183; file:// deliverable URLs get rewritten to it. |
STORAGE_API_KEY | If IPFSStorageProvider | Pinata-compatible JWT. |
STORAGE_LOCAL_PATH | No (.agent-data) | Local deliverable dir. |
Storage backend is chosen in code (pass storage_provider=), not by env var.
Security rules (important for agent flows)
- EIP-712 signing is policy-gated by default.
EVMWalletProvider.sign_typed_dataonly accepts EIP-3009TransferWithAuthorization/ReceiveWithAuthorizationagainst U-token on BSC 56/97. EIP-2612Permitand Permit2PermitSingle/PermitBatchare denylisted unconditionally (they grant unbounded allowances — a malicious 402 server could drain the wallet). Never try to bypass this in agent-reachable code;SigningPolicy.permissive()and_DANGEROUS_sign_typed_data_no_policy()are tests-only. - Never hand tool functions a raw
WalletProvider. Give them a scopedX402Signer(wallet, max_value_per_call={token: ...}, session_budget={token: ...})and always passexpected_tofrom config/on-chain registry — never from the 402 challenge body. - Custom tokens/types:
SigningPolicy.strict_default().extend(domain_allowlist={(chain_id, contract)}, primary_type_allowlist={"MyType"})— the Permit denylist still wins. claimRefundis non-pausable and non-hookable: funds are always recoverable pastexpiredAt.- Throwaway/demo keys: use
EVMWalletProvider(persist=False)so one-off keys never touch disk, and never reuse demo keys in production. Never paste production private keys into chat/logs — prefer the keystore +WALLET_PASSWORDflow.
Full rationale, decision tree, and examples: references/sdk-readme.md (Security section) and examples/security/e2e.py.
Wallet backends
EVMWalletProvider(default) — Keystore V3 (MetaMask/Geth compatible), persistent at~/.bnbagent/wallets/or in-memory withpersist=False.TWAKProvider(Trust Wallet Agent Kit) — self-custody, self-broadcasting; no raw-tx or generic EIP-712 signing; BSC only; x402 via delegated payermake_x402_payer(). Readreferences/twak.mdbefore using — unsupported calls raiseUnsupportedWalletOperation. Swap:TWAKProvider(chain="bsc")orWALLET_KIND=twak.- Custom (HSM, MPC, KMS): subclass
WalletProvider. Details:references/wallets.md.
AA / self-broadcast wallets (Privy, ZeroDev, etc.) — known integration tax
Account-abstraction wallets that submit Intents/UserOperations instead of raw signed transactions are not first-class in the SDK yet (field-tested on BSC mainnet with a Privy AA client, bnbagent==0.4.0). What to expect and the workarounds that worked:
- ERC-20
approvefails:approve_payment_tokengoes through_send_tx→ rawsign_transaction, which AA wallets don't implement (sign.transaction: wallet does not implement raw-transaction signing). Workaround: do theapprove(commerce, amount)on the payment token manually through your AA stack, then callfundwithfund_bundles_approval=True.create_job/set_budget/fundthemselves work through a custom Intent executor (subclass the executor /WalletProvider). jobIdcomes backNoneaftercreate_job: AA wallets return auser_operation_hash, not a tx hash, andcreate_jobonly parsesjobIdfrom receipt logs. Workaround: wait for inclusion, then scanjobCounterbackwards (or index recent jobs byclient+provider+description) to find your job.- For a documented self-broadcasting wallet, use
TWAKProvider(references/twak.md); Privy-style adapters are currently build-your-own.
Troubleshooting
| Error | Cause → Fix |
|---|---|
No PRIVATE_KEY and no keystore found | New wallet auto-generated, or set PRIVATE_KEY to import. |
Multiple wallets found | Set WALLET_ADDRESS=0x.... |
403 Provider mismatch | Job assigned to a different provider — check job.provider. |
409 Not FUNDED | Job already submitted/settled. |
408 Job expired | Past expiredAt; client can claimRefund, create a new job. |
402 Budget below service price | Client must fund ≥ ERC8183_SERVICE_PRICE. |
settle reverts policy pending | Dispute window not elapsed and no dispute — wait, then retry. On mainnet the window is 7 days. |
voteReject reverts not voter/not disputed | Caller not whitelisted or no dispute — use examples/voter/vote_reject.py. |
Revert selector 0xcdbc1d27 on register_job | Job already bound to a policy — re-registering reverts by design. Treat as success if job_policy(job_id) is already set. |
sign.transaction: wallet does not implement raw-transaction signing | AA/self-broadcast wallet hit the raw-tx approve path — see AA wallet section above. |
jobId is None after create_job | AA wallet returned a user-op hash, no receipt to parse — scan jobCounter backwards for your job. |
Allowance stays 0 after fund attempt | The bundled approve never landed (wrong wallet path) — manually approve(commerce, amount), then fund. |
Files in this skill
references/sdk-readme.md— full upstream SDK README (deep detail on everything above)references/architecture.md— code map, layering, invariantsreferences/twak.md— TWAK wallet support matrix and boundariesreferences/wallets.md— wallet provider deep divereferences/env.example— annotated env var referenceexamples/— copy-and-own scripts:client/(5 canonical job flows),a2a-agent/,agent-server/,voter/,twak/,x402/,security/,auto_settle.py