@bnbagent/sdk

中文文档
0.6.1发布于 2026/10/22MITTypeScriptESM + CJS构建来源已验证近 30 天下载 2649

TypeScript SDK for building on-chain AI agents on BNB Chain (ERC-8004 identity, ERC-8183 agentic commerce, x402 payments).

原始使用文档
文档目录9 节
  1. Install
  2. Package layout
  3. Quickstart
  4. (a) Client-side job lifecycle
  5. (b) Provider earn loop
  6. Wallet providers
  7. Sponsored relay fallback
  8. Environment variables
  9. Parity with the Python SDK

@bnbagent/sdk

npm Node License: MIT

TypeScript toolkit for building on-chain AI agents on BNB Chain: ERC-8004 identity, the ERC-8183 agentic-commerce protocol (escrow + evaluation + optimistic dispute policy), x402 micropayments, wallets (local keystore + pluggable executors), and the supporting core (paymaster, nonce management, tx tuning).

This is a layered port of the reference Python SDK - same protocol semantics, same on-chain deployments, idiomatic TypeScript API. See ARCHITECTURE.md for the full protocol reference; this README covers the TypeScript-specific surface and quickstart.

Install

pnpm add @bnbagent/sdk
# or
npm install @bnbagent/sdk

Requires Node.js >= 20. The package ships both ESM (import) and CommonJS (require) builds plus TypeScript types.

Package layout

The public API is split into a small Tier 1 barrel (the handful of names most integrations need) and Tier 2 subpath exports (each protocol module's full surface):

// Tier 1 - the essentials
import { ERC8183Client, EVMWalletProvider, JobStatus } from "@bnbagent/sdk";

// Tier 2 - full module surface, imported by subpath
import {
  ERC8183JobOps,
  fundedJobWatcher,
  CommerceClient,
} from "@bnbagent/sdk/erc8183";
import { ERC8004Agent, AgentURIGenerator } from "@bnbagent/sdk/erc8004";
import { X402Signer, SessionBudgetTracker } from "@bnbagent/sdk/x402";
import {
  LocalStorageProvider,
  IPFSStorageProvider,
} from "@bnbagent/sdk/storage";
import {
  LocalExecutor,
  UnsupportedWalletOperation,
} from "@bnbagent/sdk/wallets";
import { SigningPolicy, check } from "@bnbagent/sdk/signing";
import { getAddress, BNB_CHAIN_ADDRESSES } from "@bnbagent/sdk/networks";
import { SlidingWindowLimiter, RateLimitExceeded } from "@bnbagent/sdk/utils";

Subpaths available: ./erc8004, ./erc8183, ./x402, ./storage, ./wallets, ./signing, ./networks, ./utils.

The TypeScript signing checker supports nested EIP-712 structs when their dependency graph has one unambiguous root. Arrays of structs are supported; ambiguous, disconnected and cyclic multi-struct graphs are rejected. This enables explicitly opted-in Permit2 PermitWitnessTransferFrom requests through the public wallet API. The normal primary-type/domain allowlists, denylist and validity checks still run: nested signing does not grant a new payment permission. Every schema, single- or multi-struct, is validated before signing: struct and field names must be Solidity identifiers, field types must be EIP-712 atomic types (no uint/int aliases) or declared structs, optionally as arrays, and schemas are limited to 256 structs, 4096 total fields, depth 64 and 256-character identifiers. Violations raise PolicyViolation before signing.

Explicitly allowing a Permit2 domain and primary type is not a payment budget. The generic SDK policy does not pin Permit2 witness schemas, spenders, token amounts, recipients or deadline bounds. The trusted payment integration must validate those values before calling the wallet; never forward untrusted raw typed data after merely adding Permit2 to the allowlist.

Quickstart

Both snippets assume a funded wallet on bsc-testnet (get test BNB from the BNB Chain faucet) and an .env populated per the environment variables table below. Call loadEnv() once at your entrypoint to load .env/.env.local (the SDK never does this for you - see its docstring for the precedence rules).

(a) Client-side job lifecycle

The client creates a job, binds the on-chain dispute policy, funds escrow, and - once the provider submits - settles the job (permissionless: it just pulls the policy's verdict and applies it on-chain).

import {
  loadEnv,
  EVMWalletProvider,
  ERC8183Client,
  JobStatus,
} from "@bnbagent/sdk";

loadEnv(); // opt-in .env / .env.local loading

const wallet = new EVMWalletProvider({
  password: process.env.WALLET_PASSWORD!,
  // First run only - the SDK encrypts it to ~/.bnbagent/wallets/<address>.json
  // and subsequent runs only need the password.
  privateKey: process.env.PRIVATE_KEY,
});

const client = await ERC8183Client.create({
  walletProvider: wallet,
  network: "bsc-testnet",
});

const decimals = await client.tokenDecimals();
const budget = 1n * 10n ** BigInt(decimals); // 1 token

// expiredAt must clear (disputeWindow + a safety buffer) or createJob()
// throws - a job whose deadline is too close can never be submitted.
const disputeWindow = await client.policy.disputeWindow();
const expiredAt = BigInt(Math.floor(Date.now() / 1000)) + disputeWindow + 600n; // +10 min slack

const { jobId } = await client.createJob({
  provider: process.env.PROVIDER_ADDRESS!,
  expiredAt,
  description: "ERC-8183 demo: summarize this week's BSC ecosystem news",
});
console.log(`[client] createJob jobId=${jobId}`);

// Bind the OptimisticPolicy so settle() has a verdict source.
await client.registerJob(jobId!);

// Escrow the budget - auto-approves the payment token if the allowance is short.
// Zero price: for a free job either party calls `setBudget(jobId, 0n)`,
// then `fund(jobId, 0n)` moves no tokens and skips the ERC-20 approve
// entirely. See examples/client/zeroPrice.ts.
await client.fund(jobId!, budget);
console.log("[client] fund OK (Open -> Funded)");

// ... provider calls jobOps.submitResult() / client.submit() here (see below) ...

// After the dispute window elapses with no rejection, settle() finalizes
// the job as COMPLETED (or REJECTED if the policy recorded a reject vote).
await client.settle(jobId!);
const job = await client.getJob(jobId!);
console.log(`[client] settle -> ${JobStatus[job.status]}`);

(b) Provider earn loop

A headless provider agent polls for jobs funded to its address and submits a deliverable for each one. ERC8183JobOps handles verification (status, assignment, expiry, budget floor) and deliverable upload; fundedJobWatcher is a signer-free polling loop - it only detects funded jobs, so the callback decides what to do (including delegating signing elsewhere).

import { EVMWalletProvider } from "@bnbagent/sdk";
import { ERC8183JobOps, fundedJobWatcher } from "@bnbagent/sdk/erc8183";
import { LocalStorageProvider } from "@bnbagent/sdk/storage";

const wallet = new EVMWalletProvider({
  password: process.env.WALLET_PASSWORD!,
});

const jobOps = await ERC8183JobOps.create({
  walletProvider: wallet,
  network: "bsc-testnet",
  storageProvider: new LocalStorageProvider(".agent-data"),
  servicePrice: 1n * 10n ** 18n, // reject jobs budgeted below 1 token (18 decimals)
  agentUrl: process.env.ERC8183_AGENT_URL, // public base URL, used as a deliverable-URL fallback
});

await fundedJobWatcher(
  jobOps,
  async (job) => {
    const jobId = job.jobId as number;
    console.log(`[earn-loop] job ${jobId} funded, budget=${job.budget}`);

    const result = await jobOps.submitResult(
      jobId,
      `computed result for job ${jobId}`,
      { model: "my-model-v1" },
    );
    if (!result.success) {
      console.error(`[earn-loop] submit(${jobId}) failed: ${result.error}`);
      // Returning { retry: true } asks the watcher to re-validate and
      // re-fire this job on the next tick (only for transient failures).
      return { retry: result.retryable === true };
    }
    console.log(`[earn-loop] submitted ${jobId}, tx=${result.txHash}`);
  },
  { interval: 30 }, // seconds between polls
);

Wallet providers

Every protocol client signs through the WalletProvider seam - swap the provider, keep the protocol code:

EVMWalletProviderTWAKProviderAltanaWalletProviderTurnkeyWalletProvider
Custodylocal key, Keystore V3 on disktwak CLI keystore (~/.twak) + OS keychain; the key never enters this processEIP-7702 wallet == your EOA; the Altana capability reference covers the relay and session modelkey generated + held in Turnkey's AWS Nitro enclave, never leaves it; the agent holds only a local P-256 API key that stamps each request
Capabilitiessign.message/transaction/typed_data, calls.arbitrary, paymaster.sponsorsign.message, broadcast.self, intents.erc8004, intents.erc8183 (no raw signing, no x402.pay until requestExact exists)broadcast.self, calls.arbitrary, intents.erc8004, intents.erc8183 (+ x402.pay in session mode)sign.message/transaction/typed_data, calls.arbitrary, paymaster.sponsor (same surface as EVM, remote signer)
Agent containmentSigningPolicy (in-process)fixed command menu + out-of-process custody; twak's own --max-payment hard capon-chain session keys: call whitelist + spend caps + expiry, revocable in one txSigningPolicy (in-process, pre-billing) + Turnkey's server-side policy engine — root API keys bypass the server layer entirely; production needs a non-root API user + explicit ALLOW policy
Gasself-paid or MegaFuel-sponsoredmainnet auto-sponsored by twak; the SDK forwards its paymaster as --paymaster-url (twak >= v0.20.0), so sponsored testnet writes workrelay fronts gas, recovers it from the wallet (MegaFuel not involved)self-paid or MegaFuel-sponsored (broadcast is always the SDK's own RPC; Turnkey's managed broadcast is a paid feature the provider never uses)
ERC-8183 quote signingEIP-191 via walletProvider-ERC-1271 via sessionQuoteSigner(); no admin EOA or generic message-signing authority in the agentEIP-191 via walletProvider (1 billed signature per quote)
x402 paymentsX402SignerDelegated TwakX402Payer (makeX402Payer(), five-point quote precheck)Session-key payer (makeX402Payer(), SDK >= 0.4.0) after a one-time admin setup: approveX402SignatureChecker + bounded setPermit2Allowance; receiving at the wallet works fineX402Signer — works, but $0.10/signature (PAYG) at 1 req/s makes high-frequency buying uneconomical; seller/low-frequency signing is the sweet spot
Extra install-npm i @trustwallet/cli (>= 0.20.0; the local node_modules/.bin/twak is auto-resolved)pnpm add @altananetwork/sdk (optional peer, GPL-3.0-or-later, lazily imported)pnpm add @turnkey/sdk-server @turnkey/viem (optional peers, lazily imported)

Turnkey quick start (remote enclave signing; every successful signature is billed — free tier 25/month at 1 request/second, pay-as-you-go $0.10/signature):

import { TurnkeyWalletProvider } from "@bnbagent/sdk/wallets";

// env: TURNKEY_API_PUBLIC_KEY / TURNKEY_API_PRIVATE_KEY / TURNKEY_ORG_ID /
//      TURNKEY_SIGN_WITH (the wallet account's 0x ADDRESS, not a wallet id)
const wallet = TurnkeyWalletProvider.fromEnv({ expectedChainId: 97 });

const jobs = await ERC8183Client.create({
  walletProvider: wallet,
  network: "bsc-testnet",
});

The provider is a pure signer: ERC-8004/8183 writes ride the default LocalExecutor, x402 rides X402Signer, and the MegaFuel paymaster path works unchanged (enclave-verified with gasPrice=0 legacy signing). All SDK-side guards — SigningPolicy, the expectedChainId pin, input validation — run before the billable API call, so a refusal never costs quota. One vendor quirk is patched inside signTypedData: @turnkey/viem <= 0.14.34 silently serializes the EIP-712 domain as {} when the types object lacks an explicit EIP712Domain entry, so the provider always injects it (the signature would otherwise bind an empty domain). Live verification: TURNKEY_E2E=1 pnpm run e2e:turnkey (5 billed signatures; see examples/turnkey/e2e.ts).

Session quick start (admin grants once, agent runs with the session):

import {
  AltanaWalletProvider,
  defaultAgentPermissions,
  serializeSession,
} from "@bnbagent/sdk/wallets";
import { getAddress as getDeployment } from "@bnbagent/sdk/networks";

const { paymentToken, commerceProxy } = getDeployment(56);

// admin side - grant a scoped session (~$0.50-equiv BNB registration fee)
const admin = new AltanaWalletProvider({
  privateKey: process.env.PRIVATE_KEY!,
});
const session = await admin.grantSession({
  permissions: defaultAgentPermissions({
    chainId: 56,
    tokenSpend: { limit: 10n ** 18n },
  }),
  expiry: Math.floor(Date.now() / 1000) + 86_400,
});
// Session keys cannot approve tokens. Provision a bounded Commerce allowance
// from the admin side, no higher than the session token cap; zero on revoke.
await admin.setErc8183Allowance(paymentToken, commerceProxy, 10n ** 18n);
writeFileSync(".session.json", serializeSession(session), { mode: 0o600 }); // byte-exact - required

// agent side - ALTANA_SESSION_FILE=.session.json
const wallet = await AltanaWalletProvider.sessionFromEnv();
const jobs = await ERC8183Client.create({
  walletProvider: wallet,
  network: "bsc-mainnet",
});

The same session can sign negotiation quotes through the narrow sessionQuoteSigner() seam. The agent still has no admin EOA and does not gain the generic sign.message capability. See the Altana quote-signing example for trusted checker setup and acceptance-block verification.

Negotiation quotes default to the maximum SDK lifetime of 900 seconds (and are shortened further when the wallet/session expires sooner). Provider-side ERC8183JobOps requires a valid signed quote by default and verifies it at the indexed JobFunded block before doing work or submitting. Buyer edits, signature removal, late funding, and provider/chain/Commerce mismatches are permanent quote_invalid rejections. allowUnsignedJobs: true is an explicit unsafe compatibility mode for pre-signature jobs.

@altananetwork/sdk >= 0.5.0 adds three surfaces the provider exposes: the network: "bnb-testnet" preset (Altana's official chain-97 stack), balances({ tokens }) (native + ERC-20 reads; raw is the on-chain value transfers use, display is the vendor SDK's human formatting - vendor behavior passed through as-is), and ephemeral sessions - grantSession({ register: false }) skips the ~$0.50 KeyStore registration (enforcement is unchanged; the key is just invisible to registry readers like verify_authorization) and registerSessionKey(session) upgrades one to registered later, idempotently.

See the Altana capability reference for the supported surface and security boundaries. examples/altana/ contains the fee table, x402 session-payer setup, and testnet E2E.

Sponsored relay fallback

For local EVM signing, a sponsored transaction hash must become visible to the chain before the SDK trusts it. If the primary RPC never sees the hash, the SDK asks fallback RPCs from distinct origins. A same-nonce self-pay is allowed only when at least two origins agree the relay transaction is absent; two agreeing that it is present suppress the fallback, and every weaker or conflicting result fails closed with RelaySubmissionUnverifiedError. Custom BNBAGENT_FALLBACK_RPC_URLS remain supported, but entries that share an origin count as one source. Custom endpoints form part of the operator's trust boundary: choose independently operated origins, because an operator-supplied quorum can authorize the fallback. BNBAGENT_USE_PAYMASTER=0 disables sponsorship when you want to self-pay from the start.

EVMWalletProvider does not retain its construction password. Pass a password explicitly to exportKeystore(password), and call destroy() when local signing is finished. The live private key necessarily remains in process memory until then; use a remote or scoped wallet backend for higher-value deployments.

Environment variables

None of these are read automatically - call loadEnv() (from @bnbagent/sdk) at your entrypoint to load .env.local then .env, or set them however your deployment normally injects environment variables. See .env.example at the repo root for the authoritative, fully-commented reference.

VariableUsed byNotes
NETWORKresolveNetworkPreset name: bsc-testnet (default) or bsc-mainnet.
RPC_URL / RPC_URL_<NETWORK>resolveNetworkOverride the preset RPC endpoint; per-network var wins.
WALLET_PASSWORDEVMWalletProviderRequired for the EVM backend. Encrypts/decrypts the local Keystore V3 file; unused by TWAK.
PRIVATE_KEYEVMWalletProviderEVM backend, first run only - imported then encrypted to disk; remove afterward.
WALLET_ADDRESSEVMWalletProviderDisambiguates which keystore to load when several exist.
WALLET_KINDERC8183Config.fromEnvevm (default) or twak; TWAK owns its custody and does not use PRIVATE_KEY/WALLET_PASSWORD.
ERC8183_COMMERCE_ADDRESSgetErc8183ConfigOverride the AgenticCommerce kernel address (custom deployments only).
ERC8183_ROUTER_ADDRESSgetErc8183ConfigOverride the EvaluatorRouter address.
ERC8183_POLICY_ADDRESSgetErc8183ConfigOverride the OptimisticPolicy address.
ERC8183_SERVICE_PRICEERC8183Config / ERC8183JobOpsMinimum job budget (raw token units) a provider accepts.
ERC8183_AGENT_URLERC8183JobOpsPublic base URL for the agent; fallback deliverable-URL host.
ERC8183_FUNDED_POLL_INTERVALagent-server examplesPoll interval (seconds) for the funded-job scan.
ERC8183_MAX_RESPONSE_BYTES / ERC8183_MAX_METADATA_BYTESERC8183JobOps.submitResultPer-deliverable upload size caps.
ERC8004_REGISTRY_ADDRESSgetErc8004ConfigOverride the Identity Registry address.
ALTANA_SESSIONAltanaWalletProvider.sessionFromEnvSerialized Altana session JSON (contains the session key - handle as a secret). Wins over the file variant.
ALTANA_SESSION_FILEAltanaWalletProvider.sessionFromEnvPath to a file holding the serialized session (mode 0600).
STORAGE_LOCAL_PATHLocalStorageProvider.fromEnvBase directory for local storage.
STORAGE_API_KEYIPFSStorageProvider.fromEnvRequired Pinata-compatible JWT.
STORAGE_API_URL / STORAGE_GATEWAY_URLIPFSStorageProviderOptional custom pin endpoint / gateway.
BNBAGENT_MIN_GAS_PRICE_WEIminGasPriceWeiGlobal gas-price floor override (wei), all chains.
BNBAGENT_RECEIPT_TIMEOUTgetDefaultReceiptTimeoutDefault transaction-receipt wait, in seconds.
BNBAGENT_USE_PAYMASTERresolveNetwork1 forces sponsorship, 0 self-pays, and unset inherits the network preset.
BNBAGENT_RELAY_UNSEEN_TIMEOUTgetRelayUnseenTimeoutSeconds an unseen sponsored hash may remain invisible before fallback handling; 0 disables the early abort.
BNBAGENT_FALLBACK_RPC_URLSrelay verifierComma-separated RPC endpoints used to confirm whether a relay-returned hash is unseen; overrides the built-in BSC endpoint list. A decisive result requires two distinct URL origins.

Parity with the Python SDK

This package mirrors python/bnbagent module-for-module - same contract addresses, same protocol invariants, same env var names. For protocol-level detail (the ERC-8183 state machine, negotiation handshake, x402 payment flow, signing-policy threat model, wallet capability model), see ARCHITECTURE.md and the Python package's own README.md files under python/bnbagent/*/README.md - those docs are protocol reference, not Python-specific, and apply equally here.

版本与发布记录

@bnbagent/sdk 共发布 25 个版本,首次发布于 2026/07/29;过去 12 个月发布了 25 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.1latest2026/10/2217 天前
0.6.1-alpha.1alpha2026/09/231 个月前
0.6.02026/09/201 个月前
0.6.0-alpha.22026/09/181 个月前
0.6.0-alpha.12026/09/181 个月前
0.5.7-alpha.52026/09/171 个月前

关于 @bnbagent/sdk 的常见问题

@bnbagent/sdk 是什么?

TypeScript SDK for building on-chain AI agents on BNB Chain (ERC-8004 identity, ERC-8183 agentic commerce, x402 payments).

如何安装 @bnbagent/sdk?

在项目目录运行 npm install @bnbagent/sdk,或使用 pnpm add / yarn add / bun add。

@bnbagent/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bnbagent/sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@bnbagent/sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bnbagent/sdk 可以商用吗?

@bnbagent/sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bnbagent/sdk 的下载量有多少?

根据 npm 官方下载统计,@bnbagent/sdk 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,649 次,日均约 88 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bnbagent/sdk 还在维护吗?

最新版本 0.6.1 发布于 2026/10/22(17 天前);至今共发布 25 个版本,其中过去 12 个月发布了 25 个;首次发布于 2026/07/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。