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:
# 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]"
Table of Contents
- 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
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.
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=[
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']}")
Quick Start: Run an ERC-8183 Agent Server
Set up an agent server that accepts jobs, processes work, and gets paid.
Prerequisites
pip install "bnbagent[server,ipfs]"- A
.envfile with your credentials (seeexamples/agent-server/.env.example)
Option 1: Standalone App (create_erc8183_app)
# 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 8003
create_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.
Option 2: Mount on Existing App (sub-app)
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.
Endpoints
| 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. |
on_job Callback
# 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.
Settle
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.
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. |
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.
Architecture & Components
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.
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 —
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.
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/agent-server/ |
Provider | FastAPI agent that searches blockchain news via DuckDuckGo. Demonstrates create_erc8183_app(), the funded-job poll loop, and ERC-8004 registration. |
Security
- 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.
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.3.3.tar.gz.
File metadata
- Download URL: bnbagent-0.3.3.tar.gz
- Upload date:
- Size: 317.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3ebba8c58552ed7730dea6dbd077b757893da69a5d8cab7fa37b470ef29281a
|
|
| MD5 |
8e3032becb5c48823a19494f8a823e4a
|
|
| BLAKE2b-256 |
de92bac35289dbda9a5c1d5770d396f52366ea8ebbf2990c716f0b13ed22e73c
|
File details
Details for the file bnbagent-0.3.3-py3-none-any.whl.
File metadata
- Download URL: bnbagent-0.3.3-py3-none-any.whl
- Upload date:
- Size: 122.7 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 |
b6080c0f31ae4dc4236830429a69f732c1b5d361d5fcb185538b73d17ac88066
|
|
| MD5 |
25cb3d7356b019f5a3ac40309e233ff0
|
|
| BLAKE2b-256 |
f8b687f9c6e1367f054668f604728f3a95b95ba63f2dc01bd8f834d824d139fd
|