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.
Install from PyPI:
pip install bnbagentThe base package includes ERC-8004 identity registration and the ERC-8183 client stack. Install optional extras for additional features:
# ERC-8183 server components (FastAPI + Uvicorn)
pip install "bnbagent[server]"
# IPFS storage (HTTP pinning service backend, e.g. Pinata)
pip install "bnbagent[ipfs]"
# All extras
pip install "bnbagent[server,ipfs]"- What is ERC-8004?
- What is ERC-8183?
- Quick Start: Register an Agent (ERC-8004)
- Quick Start: Run an ERC-8183 Agent Server
- Quick Start: Use
ERC8183Clientfrom a Client - Configuration Reference
- Architecture & Components
- Network & Contracts
- Examples
- Security
- Troubleshooting
- License
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.
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.
| 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. |
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 │
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. |
Register your AI agent on-chain with a unique identity. This is a one-time setup.
- 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=[
AgentEndpoint(
name="ERC-8183",
endpoint="https://my-agent.example.com/erc8183/status",
version="0.1.0",
),
],
)
result = sdk.register_agent(agent_uri=agent_uri)
print(f"Agent registered! ID: {result['agentId']}, TX: {result['transactionHash']}")Set up an agent server that accepts jobs, processes work, and gets paid.
pip install "bnbagent[server,ipfs]"- A
.envfile with your credentials (seeexamples/agent-server/.env.example)
# agent.py
from bnbagent.erc8183.server import create_erc8183_app
def execute_job(job: dict) -> str:
"""Called automatically for each FUNDED job. Return the deliverable string."""
return f"Processed: {job['description']}"
app = create_erc8183_app(on_job=execute_job)
# Routes at /erc8183/negotiate, /erc8183/status, /erc8183/job/{id}, etc.# .env
WALLET_PASSWORD=your-secure-password
PRIVATE_KEY=0x... # first run only; encrypted to ~/.bnbagent/wallets/
ERC8183_AGENT_URL=http://localhost:8003/erc8183 # required for LocalStorageProvider (default)
ERC8183_SERVICE_PRICE=1000000000000000000 # 1 token (18 decimals)
# To use IPFS instead, swap to IPFSStorageProvider in your service code and set:
# STORAGE_API_KEY=your-pinning-service-jwt
# Optional knobs (see env-var table below for full reference):
# ERC8183_FUNDED_POLL_INTERVAL=30 # default poll cadence (s)
# ERC8183_NEGOTIATE_RATE_LIMIT=120 # /negotiate per-IP request budget
# ERC8183_NEGOTIATE_RATE_WINDOW=60 # rate-limit window (s)
# ERC8183_MAX_RESPONSE_BYTES=5242880 # response_content cap (5 MB)
# ERC8183_MAX_METADATA_BYTES=262144 # metadata cap (256 KB)uvicorn agent:app --port 8003create_erc8183_app() handles: wallet keystore, periodic on-chain poll for newly FUNDED jobs assigned to this provider, on-chain verification, calling your handler, uploading the deliverable to storage, and submitting on-chain. Jobs with budget < service_price are rejected with HTTP 402. Settle is permissionless — run a separate operator script to call router.settle(jobId) once the dispute window elapses.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from bnbagent.erc8183.server import create_erc8183_app
def execute_job(job: dict) -> str:
return f"Processed: {job['description']}"
erc8183_app = create_erc8183_app(on_job=execute_job, prefix="")
@asynccontextmanager
async def lifespan(app: FastAPI):
await erc8183_app.state.startup()
yield
app = FastAPI(lifespan=lifespan)
app.mount("/erc8183", erc8183_app)Starlette does not propagate lifespan events into mounted sub-apps; call erc8183_app.state.startup() from your parent lifespan to launch the funded-job poll loop.
| Method | Path | Description |
|---|---|---|
POST |
/erc8183/negotiate |
Price negotiation (off-chain). Returns a structured quote. Rate-limited per client IP. |
GET |
/erc8183/job/{id} |
Job details from the Commerce kernel. |
GET |
/erc8183/job/{id}/response |
Stored deliverable for a submitted job. |
GET |
/erc8183/job/{id}/verify |
Verify a job is FUNDED, assigned to this provider, not expired, budget ok. |
GET |
/erc8183/status |
Agent wallet, contract addresses, service price, payment token, decimals. |
GET |
/erc8183/health |
Liveness check. |
# Sync or async, with or without per-job metadata:
def on_job(job: dict) -> str: ...
async def on_job(job: dict) -> str: ...
def on_job(job: dict) -> tuple[str, dict]: ...
async def on_job(job: dict) -> tuple[str, dict]: ...job contains: jobId, description, budget, client, provider, evaluator, status (always FUNDED), expiredAt, hook.
router.settle(jobId) is permissionless — any party can finalise a submitted job once its dispute window elapses. The SDK does not run an in-server settle loop; operators are expected to run a separate script that polls verdicts and calls ERC8183Client.settle(jobId) when ready.
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.COMPLETEDapprove_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.
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 reachedSee examples/client/ for the five canonical flows (happy, dispute-reject, stalemate-expire, never-submit, cancel-open).
| 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. |
ERC8183_NEGOTIATE_RATE_WINDOW |
No | 60 |
Sliding-window length for /negotiate rate limit, in seconds. |
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.
See ARCHITECTURE.md for the full code map, module system, 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/server/— FastAPI factory and async job ops with funded-job poll loop.
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 —
BNBAgentConfig/ERC8183Configacceptprivate_key=directly and wrap it intoEVMWalletProvider(persist=False)in__post_init__, immediately zeroing the plaintext field. - Keystores written with
0o600permissions (directory0o700).
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.
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.
| 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).
| Contract | Address |
|---|---|
| Identity Registry (ERC-8004) | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 |
| AgenticCommerce (APEX) | 0xea4daa3100a767e86fded867729ae7446476eba6 |
| EvaluatorRouter | 0x51895229e12f9876011789b04f8698af06ccd6da |
| OptimisticPolicy | 0x9c01845705b3078aa2e8cff7520a6376fd766de5 |
| 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/agent-server/ |
Provider | FastAPI agent that searches blockchain news via DuckDuckGo. Demonstrates create_erc8183_app(), the funded-job poll loop, and ERC-8004 registration. |
- 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 with HTTP 402 at
/status,/job/{id}/verify, and at submit time insidesubmit_result(). - 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.
| 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. |
MIT License — see LICENSE for details.