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:

# IPFS storage (HTTP pinning service backend, e.g. Pinata)
pip install "bnbagent[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.

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. fund and settle are 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 inside settle (→ complete); (2) the only ERC-20 approve is on the payment token (not an ERC-8183 function), sent by fund() 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:

  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=[
        # 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/send fronting NegotiationHandler, with on-chain discovery.
  • examples/agent-server/ — full HTTP provider (FastAPI factory lives in the example at src/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) — 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 (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.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/job_ops.pyERC8183JobOps + 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 a private_key + wallet_password pair.
  • Auto-wrap — ERC8183Config (and other AgentConfig subclasses) 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).

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_data are absent) — twak signs ERC-8004 / ERC-8183 / x402 payloads internally and only exposes high-level operations. Anything that needs direct EIP-712 (e.g. an X402Signer you construct yourself) requires EVMWalletProvider.
  • 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); x402 request is 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 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/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 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 by verify_job() and again at submit time inside submit_result() (the HTTP example surfaces this as a 402 response).
  • 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.

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


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.4.0.tar.gz (480.2 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.4.0-py3-none-any.whl (193.8 kB view details)

Uploaded Python 3

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

Hashes for bnbagent-0.4.0.tar.gz
Algorithm Hash digest
SHA256 a84a284d232e70c940dddb999a205103e42b7c1bd165c8483f119f76d07f5608
MD5 eb120747e8084395252a802b9232a881
BLAKE2b-256 b362d931fd4583a2d2f9a0242b3fc40ae0fad95bbe3667a63b3f006a3d7efdfe

See more details on using hashes here.

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

Hashes for bnbagent-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9e2e2728a45239957eef49939cb6e3749d27b173bd7644e514d1b1dcf3ea3711
MD5 d2a459dfac7229703ec6861a9aec5587
BLAKE2b-256 48f63b873b91122bd16086002eec94f1eb81d462256980d1adab2eb5b97bbf87

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