Skip to main content

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?

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:

  1. AgenticCommerce — the ERC-8183 kernel. Owns job state and escrow.
  2. EvaluatorRouter — the routing layer. Binds each job to a policy; doubles as job.evaluator and job.hook. settle(jobId) is permissionless and pulls the verdict.
  3. OptimisticPolicy — the reference policy. Silence past the dispute window is implicit approval. A client-raised dispute triggers a whitelisted-voter quorum: enough voteReject calls 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

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) — Approve max(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 exactly amount (most conservative).
  • approve_floor=X — Approve max(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.pyERC8183Client facade (most callers use this).
  • bnbagent/erc8183/commerce.pyCommerceClient (low-level Commerce kernel).
  • bnbagent/erc8183/router.pyRouterClient (low-level Router).
  • bnbagent/erc8183/policy.pyPolicyClient (low-level OptimisticPolicy).
  • bnbagent/erc20/client.pyMinimalERC20Client — 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 a private_key + wallet_password pair.
  • Auto-wrap — BNBAgentConfig/ERC8183Config accept private_key= directly and wrap it into EVMWalletProvider(persist=False) in __post_init__, immediately zeroing the plaintext field.
  • Keystores written with 0o600 permissions (directory 0o700).

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 to STORAGE_LOCAL_PATH (default .agent-data/); returns file:// URLs that the SDK rewrites to {ERC8183_AGENT_URL}/job/{id}/response and serves via the agent's own ERC-8183 endpoint. Requires ERC8183_AGENT_URL.
  • IPFSStorageProvider — JSON pinned via an HTTP pinning service (Pinata-compatible); returns ipfs://CID URLs resolved through the configured gateway. Requires STORAGE_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 keysEVMWalletProvider uses Keystore V3; plaintext keys are cleared from memory after import.
  • Submit-time verificationsubmit_result() re-verifies FUNDED, assignment, expiry, and budget >= service_price before every on-chain submission.
  • Budget protection — Underpriced jobs are rejected with HTTP 402 at /status, /job/{id}/verify, and at submit time inside submit_result().
  • Permissionless settlerouter.settle is callable by anyone. The SDK does not gatekeep settlement; operators run their own settle script when ready.
  • Non-pausable refundclaimRefund on the kernel is intentionally not pausable and not hookable: funds can always be reclaimed past expiredAt.
  • Storage permissionsLocalStorageProvider uses 0600/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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bnbagent-0.3.1.tar.gz (316.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bnbagent-0.3.1-py3-none-any.whl (122.2 kB view details)

Uploaded Python 3

File details

Details for the file bnbagent-0.3.1.tar.gz.

File metadata

  • Download URL: bnbagent-0.3.1.tar.gz
  • Upload date:
  • Size: 316.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bnbagent-0.3.1.tar.gz
Algorithm Hash digest
SHA256 2a84a550c70826f7597f4e1700f9e025e07b7c319ba9f218732dd19764d56c49
MD5 97e62cd545068fe77186bd9a9704e2e5
BLAKE2b-256 97de4f585e0d2b00b6d644e1d1c41aec05dfa16282e034cc8567eff3b3aff893

See more details on using hashes here.

File details

Details for the file bnbagent-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: bnbagent-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 122.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bnbagent-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 55ee817be23492168bcf0e1a4b548fb3bdbf9669c5512add2f5a056e2a034801
MD5 876407f992969c57d6032be90af92c56
BLAKE2b-256 0f09ef1f6be798048c33ee58598eeb300359b331da65c5529a93eea883218f32

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page