# @agentxv2/sdk v0.11.9

**Decentralized AI Agent Platform SDK** — E2E encryption, on-chain subscriptions, ReAct AgentLoop, multi-tenant LLM providers, A2A multi-agent interop (off-chain + user-wallet-signed on-chain rails), IPFS upload, MCP remote tools, chain-data batch query, hosted conversation sessions & parallel tasks, agent application categories, unified multi-rail payments.

```
Agent = Prompt + Skills[] + MCP
```

---

## Installation

The current release **0.11.9** is built on the **0.10.0 complete feature release** (agent application categories, sessions & parallel tasks, streaming tool_call fix, typed `onchain_approval_required` SSE event, unified `/api/v1/payments` endpoint) — with the payment engine on the InfraX-maintained `@0xinfrax/payments` `^0.1.4`. R17.6: `0.1.3` restores the a2a / period rails inside the engine; `0.1.4` adds escrow passthrough / ERC20 deposit / structured 402 errors; the AgentX Gateway delegates to the module rails while keeping the HTTP contract identical, so **B-side callers see zero change**. Since **0.11.5** (R19.3) the SDK also ships `TenantPlanPayments` — platform subscription-tier purchases (tenant plans) through the unified payments endpoint — since **0.11.6** the `BillingClient` for B-end balance pre-checks (R19.7 companion), since **0.11.7** the `AgentWalletConfig` — agent-owned MPC wallet management (bind an agent to an InfraX MPC wallet + email-code payment-session unlock) so A2A per-call fees auto-pay server-side, since **0.11.8** the **A2A Session Key auto-pay** surface — end-user wallet-authorized `x402-pay` session keys that auto-fund the per-call fee when the x402 ledger balance is short (see the "A2A Delegated Tasks & Session Key Auto-pay" section), and since **0.11.9** `AgentWalletConfig` gains a **tenant-scoped `apiKey` mode** — manage the MPC wallets of your *own* agents with a tenant API Key instead of the platform `ADMIN_KEY` (SK-1 permission delegation, dual-rail auth on `/api/v1/admin/agent-payers`). Install and use:

```bash
# latest (recommended) — 0.11.9
npm install @agentxv2/sdk

# or pin the exact release
npm install @agentxv2/sdk@0.11.9
```

### Peer Dependencies

| Package | Version | Required |
|---------|---------|----------|
| `react` | ^18 or ^19 | yes |
| `wagmi` | ^2.0 | optional (React hooks only) |
| `@tanstack/react-query` | ^5.0 | optional (React hooks only) |
| `viem` | ^2.0 | optional (chain reader only) |

---

## Quick Start

### 1. Use an Agent with AgentLoop (ReAct autonomous tool calling)

```ts
import { AgentRunner, AgentLoop, OpenAIProvider, GatewayProvider } from '@agentxv2/sdk'

const runner = new AgentRunner({ reader, wallet })
const ctx = await runner.useAgent(42)

// Mode A: Pure frontend — direct LLM API
const provider = new OpenAIProvider({ apiKey: 'sk-...', model: 'gpt-4o' })

// Mode B: SaaS multi-tenant — via AgentX Gateway (API key never in browser)
const provider = new GatewayProvider({
  gatewayUrl: 'https://agentx.0xainet.top',   // 生产域名；本地开发可填 http://localhost:3090
  accessToken: 'jwt...',
  keySource: 'platform',
})

const loop = new AgentLoop({
  ctx,
  llmProvider: provider,
  maxIterations: 5,
  contextBudget: 8000,           // NEW: auto-summarize when exceeding token budget
  memory: { enabled: true },     // NEW: pgvector cross-session memory (default: disabled)
  trace: { enabled: true },      // NEW: structured observability (default: disabled)
  onTextDelta: (delta) => appendAssistantMessage(delta),
  onToolCall: ({ name, arguments: args }) => showToolBubble(name, args),
  onToolResult: ({ name, result }) => updateToolBubble(name, result),
})

await loop.run('Analyze this contract for vulnerabilities')
```

### 2. React Hook

```tsx
import { useAgentRunner } from '@agentxv2/sdk/react'

function ChatPage({ agentId }: { agentId: number }) {
  const { ctx, isLoading, error } = useAgentRunner({ agentId })
  if (isLoading) return <div>Loading...</div>
  return <ChatInterface prompt={ctx!.prompt} skills={ctx!.skills} />
}
```

### 3. Publish an Agent (IPFSUploader + publishAgent pipeline)

```ts
import { IPFSUploader, publishAgent } from '@agentxv2/sdk'

const uploader = new IPFSUploader({ pinataJwt: 'eyJ...' })

const result = await publishAgent({
  agent: {
    name: 'Solidity Auditor',
    description: 'AI agent that audits Solidity smart contracts',
    version: '1.0.0',
    tags: ['security', 'audit'],
    category: 'security',               // v0.9.4: application category — see AGENT_CATEGORIES
    capabilities: ['smart_contract_audit'],
    supportedTasks: ['audit'],
    communicationProtocol: 'mcp',
    authenticationMethod: 'ecdsa',
    pricing: { type: 'subscription', amount: '10', currency: '', period: 'month' },
    prompt: 'You are an expert Solidity auditor...',
    skills: [{ name: 'audit', description: 'Audit a contract', version: '1.0', inputSchema: {...} }],
    mcp: { type: 'http', url: 'https://my-mcp.example.com/mcp' },
  },
  publicKey: '0x04abc...',
  uploader,
})

// 3. Mint Agent NFT on-chain
await registry.register(`ipfs://${result.publicCid}`, [
  { key: 'encryptedPayloadCid', value: result.encryptedCid },
  { key: 'eciesEncryptedKey', value: result.eciesEncryptedKeyHex },
])
```

> **Application category (v0.9.4)** — publishing agents now takes a `category` field (one of `AGENT_CATEGORIES`: `operations` / `customer-service` / `sales` / `personal-assistant` / `coding` / `server-monitoring` / `airdrop` / `quant-trading` / `data-analysis` / `content` / `security` / `finance` / `other`). It is written into the public metadata + on-chain attrs and drives Marketplace category filtering. The Studio UI enforces it as required; SDK-level it is optional for backward compatibility (agents without it fall back to `other`). `getAllAgents()` / `getAgentMetadata()` return the resolved `category`.

---

## Encryption & Decryption (Core)

The SDK provides a full E2E encryption pipeline using AES-256-GCM + ECIES (secp256k1).

### Low-Level Crypto

```ts
import {
  aesEncrypt, aesDecrypt,
  eciesEncrypt, eciesDecrypt,
  generateAesKey, generateKeyPair,
  randomBytes,
} from '@agentxv2/sdk/core'

// Generate keys
const keyPair = generateKeyPair()
// → { privateKey: '0x...', publicKey: '0x04...' }
const aesKey = generateAesKey()
// → 64-char hex string (32 bytes)

// AES-256-GCM encrypt/decrypt
const ciphertext = aesEncrypt('Hello, AgentX!', aesKey)
const plaintext = aesDecrypt(ciphertext, aesKey)

// ECIES encrypt/decrypt (key wrapping)
const wrapped = eciesEncrypt(aesKey, keyPair.publicKey)
const unwrapped = eciesDecrypt(wrapped, keyPair.privateKey)
```

### High-Level Payload Encryption

```ts
import { encryptPayload, decryptPayload, packAgentForPublish } from '@agentxv2/sdk/core'

// Publisher: encrypt agent payload for a subscriber
const encrypted = encryptPayload({
  prompt: 'You are a DeFi analyst...',
  skills: [{ name: 'audit', ... }],
  mcp: { type: 'http', url: '...' },
}, subscriberPublicKey)
// → { aesKeyHex, eciesEncryptedKeyHex, encryptedCid }

// Subscriber: decrypt with private key
const decrypted = decryptPayload(encrypted, subscriberPrivateKey)
// → { prompt, skills, mcp }

// Package agent ready for on-chain registration
const pack = packAgentForPublish(agentPayload, creatorPublicKey)
```

### Wire Format

```
AES-256-GCM:  base64( IV[12] || ciphertext || authTag[16] )
ECIES:        hex( ephemeralPub[33] || IV[16] || ciphertext || MAC[32] )
```

---

## IPFS Upload

```ts
import { IPFSUploader } from '@agentxv2/sdk/ipfs'
// or: import { IPFSUploader } from '@agentxv2/sdk'

const uploader = new IPFSUploader({
  pinataJwt: 'eyJ...',              // Pinata JWT token
  // customEndpoint: 'https://my-ipfs.example.com/api/v0/add',
  // customApiKey: '...',
  gatewayUrl: 'https://ipfs.io',     // default
  namePrefix: 'agentx-',
  timeoutMs: 30_000,                // request timeout
})

// Upload JSON
const { cid, url } = await uploader.uploadJSON(
  { hello: 'world' },
  { name: 'test-data', keyvalues: { app: 'agentx' } }
)

// Upload encrypted agent payload
const encrypted = await uploader.uploadEncryptedPayload(
  { encrypted: true, algorithm: 'AES-256-GCM', data: '...' },
  'my-agent'
)

// Get public gateway URL from CID
const publicUrl = uploader.getUrl('QmXxx...')
// → https://ipfs.io/ipfs/QmXxx...
```

---

## MCP Connector (Remote Tool Execution)

```ts
import { MCPConnector } from '@agentxv2/sdk/mcp'
// or: import { MCPConnector } from '@agentxv2/sdk'

// Connect to any MCP server
const connector = new MCPConnector({
  transport: 'http',                        // 'http' | 'sse' | 'stdio'
  url: 'https://my-mcp.example.com/mcp',
  authType: 'ecdsa',                        // optional: ecdsa signature auth
  privateKey: '0x...',                      // required for ecdsa auth
})

// Discover available tools
const tools = await connector.listTools()
// → [{ name: 'get_balance', description: '...', inputSchema: {...} }, ...]

// Execute a tool remotely
const result = await connector.callTool('get_balance', {
  address: '0x1234...',
  chainId: 19505,
})
// → { token: 'ETH', balance: '1.5' }
```

---

## ConversationClient (v0.8.7) — Remote Conversation Service

Streams agent conversations from the hosted **Conversation Service** via the Gateway (`POST /api/v1/agent/runs`, SSE). Auth requires **either** a tenant `apiKey` (`X-Api-Key`) **or** a Gateway `accessToken` (`Authorization: Bearer` — wallet-signed login). Both credentials unlock the **same** REST surface — single-turn chat plus sessions & parallel tasks — gated uniformly by the tenant's P9 capability bits; a B-end integration key (`agentx_...`) alone is sufficient, **no second key is needed** (only the MCP channel and on-chain operations require a registered-user JWT / the user's own wallet). The client also auto-sends `X-End-User-Id` (end-user memory isolation), `X-Llm-Api-Key` + `X-Llm-Endpoint` + `X-Llm-Model` (stateless BYOK override — **your own key AND endpoint AND model**, e.g. DeepSeek). **BYOK is the recommended pattern for callers**: traffic then runs against your own LLM account, and the platform fallback key is used only when no key is provided.

> **v0.8.6 — stored BYOK (`tenantKeyId`)**: each chat/stream request can pass `tenantKeyId` to use a tenant-owned API key already stored & AES-encrypted on the Gateway (managed via Settings → Own LLM Keys, backed by `/tenant/keys`). The Gateway resolves the key server-side and injects it as `X-Llm-Api-Key` (priority over request-level headers) — the plaintext key never leaves the server. This complements the stateless `llmApiKey` override (request-level, highest priority). **`tenantKeyId`s are strictly tenant-scoped**: after rotating your `agentx_` key or switching tenants, re-store a BYOK via `POST /api/v1/tenant/keys` with the new key and update your `tenantKeyId` — reusing another tenant's ID returns `400 Tenant API key not found or inactive`.
> **v0.8.7 — sessions & parallel tasks**: `createSession()` / `createTask()` (returns a `taskId` immediately, runs in the background) / `getTask()` / `listTasks()` / `cancelTask()` + `getCapabilities()`. When the tenant/plan disallows multi-task (P9 capability gate), `createTask()` is rejected with HTTP 403 `{ code: "PARALLEL_TASKS_DISABLED" }` — surfaced as `ConversationTaskError` (`.status` / `.code`); callers should degrade to single-turn `chat()`.
> **2026-08-08 — B-end keys clarified**: parallel-task capability is now **uniform for all tenants**. A B-end integration key (`agentx_...`) is no longer restricted to chat — it can create sessions/tasks exactly like a registered-user JWT, controlled by the same P9 capability bits (`effective = tenant.allow_parallel_tasks ?? plan.features.parallel_tasks ?? true`). `403 PARTNER_TASKS_DISABLED` no longer exists; a disabled tenant gets `403 PARALLEL_TASKS_DISABLED` and should fall back to `chat()` as above. **One `agentx_` key is enough** — callers do not need a second (wallet-signed) credential for sessions/tasks. Partner task creation additionally **requires a BYOK LLM key** (`X-Llm-Api-Key` header, `llmApiKey`, or stored `tenantKeyId`) — otherwise `400 { code: "LLM_KEY_REQUIRED" }` — so background work never drains the platform key budget. This gate applies to **parallel tasks created via REST/SDK** (`POST /sessions/:id/tasks`); platform-managed background paths (user schedules, orchestration triggers) bypass it and use the stored `tenantKeyId` or the platform fallback key.
>
> **MCP boundary (generic)**: the "registered-user `access_token` only" rule applies to the **AgentX platform MCP** (Gateway `/mcp`) — its 6 `agentx_gateway_*` conversation/task tools (`agentx_gateway_chat`, `create_session`, `create_task`, `get_task`, `list_tasks`, `cancel_task`) require a registered-user JWT and reject B-end `agentx_` keys (R14). MCP servers **you deploy yourself** (an in-house agent MCP, a RAG MCP, etc.) are outside this boundary — their auth is yours to configure (anonymous, tenant key, or anything else). Use REST or `ConversationClient` if you only hold a B-end key. **B-end end-users (e.g. aihunter's customers) are fully covered by REST + `X-End-User-Id: 0x<wallet>`** (end-user subscription proxying) — MCP stays JWT-only for B-end callers by design; bridging "B-end user → AgentX JWT" for MCP would be a separate design item.
>
> **B-end MCP conversation — alternative paths (2026-08-11)**: if a B-end caller needs *MCP-native* conversation for its end-users, use one of these instead of waiting for a "B-end user → AgentX JWT" bridge (kept as a future design item, **not implemented**):
> 1. **Self-hosted MCP wrapper** (most common — the RAG MCP pattern): deploy your own MCP server that exposes a conversation tool and internally calls REST with `X-Api-Key` + `X-End-User-Id`. Auth is fully under your control — this lives outside the platform boundary.
> 2. **Direct REST for HTTP-capable AIs**: if the calling AI supports HTTP tool invocation, call `/api/v1/agent/runs` (or the SDK `ConversationClient`) directly — a single `agentx_` key, with `X-End-User-Id` subscription proxying included.
> 3. **Non-conversation MCP tools are key-usable as-is**: 32 of the 38 platform MCP tools (on-chain read/write) accept B-end `agentx_` keys — only the 6 conversation/task tools require a registered-user JWT. B-end callers can already use those 32 over MCP without any bridge.
>
> **B-end end-user auth over MCP conversation — how it works (2026-08-11)**: the 6 conversation/task tools authenticate *only* via the `access_token` argument — a registered-user JWT issued by wallet-signed login (EIP-191). The MCP executor reads `access_token`, drops the call with `access_token (registered-user JWT) is required` if absent, and forwards it as `Authorization: Bearer` to the Gateway REST channel; B-end `agentx_` keys are rejected on these tools (R14). Subscription gating therefore resolves against the **JWT holder's wallet**. A B-end end-user who is not a registered AgentX user has **no wallet-signed JWT** and cannot call these tools directly — that is by design: their identity (and subscription) is proxied through the B-end key + `X-End-User-Id: 0x<wallet>` on REST instead. If MCP-native conversation for such end-users is ever required, the bridge keeps the same semantics as REST — exchange (B-end key + end-user subscription check) for a **short-lived scoped JWT** (`sub=partner-<slug>`, `end_user=0x<wallet>`, TTL ~5 min, no refresh) so the MCP tools still do their subscription check against the end-user wallet. Until then, paths 1–3 above apply.
>
> **B-end end-user auth over MCP conversation — step-by-step flow (2026-08-11)**: the same call goes through two distinct auth paths depending on the caller's credential:
>
> * **Registered user (JWT)** — `agentx_gateway_chat` with `access_token=eyJ...`:
>   1. MCP executor calls `gatewayAuthHeaders()` → token present → `Authorization: Bearer <JWT>`.
>   2. Forwards to Gateway `POST /api/v1/agent/runs`.
>   3. Gateway resolves the access subject as the **JWT holder's wallet**, then `canAccessAgent(wallet, agentId)` checks on-chain ownership/subscription — `403 AGENT_ACCESS_DENIED` if the JWT wallet holds no subscription to the target agent.
>   4. SSE stream returned, aggregated into the MCP tool result `{ reply, tool_calls }`.
> * **B-end end-user (partner key + proxied wallet)** — the *same* conversation over **REST**:
>   1. `X-Api-Key: agentx_...` + `X-End-User-Id: 0x<endUserWallet>` → `POST /api/v1/agent/runs`.
>   2. `resolveAccessSubject()` (see [gateway agent-access.ts](gateway/src/services/agent-access.ts)) detects `kind=partner` + hex `endUserId` and substitutes the **end-user's wallet** as the access subject.
>   3. `canAccessAgent(endUserWallet, agentId)` gates on the **end-user's** on-chain subscription (not the partner tenant's).
>   4. Same SSE stream, same `{ reply, tool_calls }` shape — the two paths differ **only in credential exchange**, never in conversation behavior.
>
> Decision rule: a B-end caller holds only `agentx_` keys → use REST (path 1 or 2 above); a registered user holds a wallet-signed JWT → MCP is available as-is; a B-end caller that insists on MCP-native conversation for unregistered end-users → future bridge (above), not yet implemented.
>
> **B-end key vs user JWT — sessions/tasks behavior (v0.10.1)**: both credentials are gated by the same P9 capability bits and can create sessions/parallel tasks; the differences are (1) **access subject** — a B-end key authorizes as the partner tenant, or as a proxied end-user wallet when `endUserId` is a `0x` address; a JWT authorizes as the user's own wallet; (2) **task LLM key** — partner tasks may use their own BYOK key (`X-Llm-Api-Key` / `llmApiKey` / `tenantKeyId`), or fall back to the platform key, which is **metered against the tenant's plan quota** (exact token counts from the task's `done` event — via SSE for streamed tasks and via a completion callback for background tasks); (3) **platform MCP chat/task tools** and **on-chain A2A/publish/subscribe** are available to JWTs only. A B-end key covers all REST chat + parallel tasks; a JWT additionally covers MCP and on-chain.
>
> **`endUserId` is always optional — omitting it is NOT rejected** (2026-08-08 clarification): without it the access subject falls back to the **tenant's own wallet** — for a registered user (kind=user) that *is* the user's wallet (works naturally); for a partner tenant it simply means **no end-user proxying** (the `partner-*` address is not an on-chain address, so subscription-gated agents return `403 AGENT_ACCESS_DENIED` when the chain check fails — not a "missing endUserId" rejection). A non-`0x` `endUserId` is used for memory isolation only. There is **no "must send endUserId" enforcement** anywhere.

```ts
import { ConversationClient } from '@agentxv2/sdk/conversation'

const client = new ConversationClient({
  gatewayUrl: 'https://gateway.example.com',   // Gateway base URL (not the conversation service directly)
  apiKey: 'agentx_...',                         // tenant API key issued after registration (OR accessToken below)
  // accessToken: 'eyJ...',                     // gateway JWT from wallet-signed login (alternative to apiKey)
  endUserId: 'user-123',                        // optional: per-end-user memory isolation
  llmApiKey: 'sk-...',                          // optional: BYOK — your own LLM key (highest priority)
  llmEndpoint: 'https://api.deepseek.com/v1',   // optional: endpoint for llmApiKey (default OpenAI)
  llmModel: 'deepseek-v4-pro',                  // optional: model for llmApiKey (default gpt-4o)
  timeoutMs: 120_000,                           // optional: stream timeout (default 120s)
})

// Stream events (text / tool_call / tool_result / thinking / clarification / onchain_approval_required / done / error)
const controller = new AbortController()        // optional: external stop (user "Stop" button)
for await (const event of client.stream({
  agentId: 42,
  message: 'Analyze this contract',
  enableMemory: true,
  history: [{ role: 'user', content: 'hi' }],
  tenantKeyId: 'key-01HX...',               // v0.8.6: BYOK via a stored tenant-owned API key (Settings → Own LLM Keys)
}, { signal: controller.signal })) {
  switch (event.type) {
    case 'text':           appendDelta(event.content!); break
    case 'tool_call':      showToolBubble(event.toolName!, event.toolArgs); break
    case 'tool_result':    updateToolBubble(event.toolName!, event.toolResult); break
    case 'thinking':       setThinking(event.content!); break
    case 'clarification':  askUser(event.question!); break  // request was ambiguous — prompt the user
    case 'onchain_approval_required':
      // v0.9.6+: the agent requested an auditable on-chain A2A delegation.
      // The USER must approve it in their own wallet (they pay the gas and
      // become the on-chain client). Show a wallet modal with
      // event.approval = { targetAgentId, taskType, inputData }.
      openWalletModal(event.approval!); break
    case 'done':           onDone(event.usage); break
    case 'error':          onError(event.error!); break
  }
}

// Or aggregate into a single result
const result = await client.chat({ agentId: 42, message: 'Hello' })
// → { text, toolCalls: [{ name, arguments, result }], usage, iterations }
// When the service interrupts an ambiguous request, result.clarification carries
// the clarifying question and no tools were run:
if (result.clarification) {
  const answer = await promptUser(result.clarification)
  const retry = await client.chat({ agentId: 42, message: answer, history: [...prevHistory, ...] })
}

// Inline mode — no AgentX agent needed; inject your own MCP/HTTP tools (e.g. RAG)
const ragResult = await client.chat({
  message: '根据知识库回答：AgentX 支持哪些链？',
  prompt: '你是客服助手，回答前先调用 rag_query 检索知识库。',
  skills: [{
    name: 'rag_query',
    description: 'Retrieve relevant chunks from the knowledge base',
    inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
    execution: { type: 'mcp', endpoint: 'https://your-rag-mcp.example.com/mcp', toolName: 'rag_query' },
  }],
  enableMemory: false,
})
```

> Auth: the `apiKey` set in the constructor is sent automatically as `X-Api-Key` (tenant API key) — the RAG example above needs no per-request credentials. Your RAG MCP/HTTP `execution.endpoint` stays under your control: secure it with your own auth, the service only forwards the call (30s timeout).
> Sub-path import: `@agentxv2/sdk/conversation`. Server-side API & headers documented in [`CONVERSATION_SERVICE.md`](../CONVERSATION_SERVICE.md).

### Sessions & Parallel Tasks (v0.8.7)

Create a dialog session, fire multiple tasks into it, poll them in the background and cancel when needed:

```ts
import { ConversationClient, ConversationTaskError } from '@agentxv2/sdk/conversation'

const client = new ConversationClient({ gatewayUrl: 'https://gateway.example.com', accessToken: 'eyJ...' })

// (optional) check the integrator's capability first — when false, use single-turn chat() instead
const caps = await client.getCapabilities()
if (!caps.parallelTasks) {
  const r = await client.chat({ agentId: 42, message: 'hello' })
}

const session = await client.createSession({ title: 'Audit' })          // dialog container (idempotent)
const t1 = await client.createTask({ sessionId: session.id, agentId: 42, message: 'Analyze contract A' })
const t2 = await client.createTask({ sessionId: session.id, agentId: 42, message: 'Analyze contract B' })
// → both return immediately with { id, status: 'queued' } — execution runs in the background
const tasks = await client.listTasks(session.id)                        // all tasks of the session

let task = await client.getTask(t1.id)                                  // poll until terminal
// task.status: queued → running → done / error / cancelled

try {
  await client.cancelTask(t2.id)                                        // cancel queued/running task
} catch (err) {
  if (err instanceof ConversationTaskError && err.code === 'PARALLEL_TASKS_DISABLED') {
    // tenant/plan disallows multi-task → fall back to single-turn chat()
  }
}
```

> **B-end subscription proxying** (v0.10.1, Gateway 2026-08-08): a partner caller can authorize by an end-user's subscription by passing that user's wallet via `endUserId` (`0x<wallet>`; `createSession` / `createTask` params, or `X-End-User-Id` header). The Gateway then checks ownership/subscription against that wallet instead of the partner tenant itself — so "my end-user already subscribed → I may chat on their behalf". Non-`0x` end-user ids remain memory-isolation-only. `stream(params)` accepts `params.endUserId` too (per-request override of the constructor-level id).

---

## On-Chain Data (v0.8.1) — Batch Query + Subscription Writes + Event Stream

Replaces hand-rolled ethers.js + manual ABI/parseLog code. All methods accept viem `PublicClient` / `WalletClient` (chain-agnostic).

> 完整接入样例（SDK / MCP / REST 三通道 + 关键约定）：[docs/sdk-integration-example.md](../docs/sdk-integration-example.md)
> 可运行的 SDK 链上读取完整样例（生产地址）：[examples/sdk-chain-read.ts](../examples/sdk-chain-read.ts)
> 可运行的 SDK 写操作样例（创建套餐，需私钥）：[examples/sdk-create-plan.ts](../examples/sdk-create-plan.ts)

### IdentityRegistry — batch read

```ts
import { AgentRegistry } from '@agentxv2/sdk'

const registry = new AgentRegistry({ contractAddress, publicClient, walletClient })

const total = await registry.totalAgents()            // reads totalAgents() — replaces binary search

const agents = await registry.getAllAgents({
  fromId: 1,                  // default 1
  // toId: 100,               // default: totalAgents()
  activeOnly: true,           // default false — metadata.isActive === true
  capabilities: ['trading'],  // AND filter on metadata.capabilities
  batchSize: 10,              // RPC batching (default 10)
})
// → [{ agentId, owner, tokenURI, metadata: { name, description, capabilities, skills, isActive }, createdAt }]

const meta = await registry.getAgentMetadata(1)
// → { name, description, encryptedPayloadCid, eciesEncryptedKey, publicPayloadCid,
//     capabilities, skills, isActive }
```

> **v0.8.1 容错解析**：tokenURI 可能因合约 bug 损坏（base64 尾部垃圾 / JSON 未闭合）。
> `getAllAgents()` / `getAgentMetadata()` 会自动清理尾部垃圾、补齐未闭合引号/花括号，
> 仍失败时以 regex 兜底提取 `name`，最终回退为 `Agent {id}`——与 Gateway indexer 行为一致，
> 不会因单条损坏数据导致整批查询失败。

### SubscriptionManager — write + event-parsed results

```ts
import { SubscriptionManager } from '@agentxv2/sdk'

const sm = new SubscriptionManager({ contractAddress, publicClient, walletClient })

// period MUST be one of 'day' | 'week' | 'month' | 'year' — the only values the
// contract maps to real durations. 'monthly'/'quarterly'/'yearly' silently become
// 30 days on-chain, so they are rejected at runtime.
const { planId, txHash } = await sm.createPlan({
  agentId: 42,
  price: 5000000000000000n,   // wei
  period: 'month',
  payToken: '0x0000...',      // default: native token
  trialDays: 0,
})

const sub = await sm.subscribe(planId, { valueWei: 5000000000000000n })
// → { subscriptionId, txHash, subscriber, agentId, expiresAt }  // parsed from Subscribed event

const combined = await sm.createPlanAndSubscribe({ agentId: 42, price: 1n, period: 'day' })
// → { planId, subscriptionId, txHash, subscriber, agentId, expiresAt }
```

> **v0.8.2 写操作签名修复**：`createPlan()` / `subscribe()` / `releaseFunds()` / `cancel()`
> 现在优先使用完整的 viem `walletClient.account`（含签名能力），支持**本地私钥签名**场景
> （`privateKeyToAccount` → `eth_sendRawTransaction`）；浏览器钱包（MetaMask 等，
> json-rpc account）行为不变。此前传入裸地址字符串会走 `eth_sendTransaction`
> （仅节点托管账户），本地签名时被 RPC 拒绝（`unknown account`）。
> 写操作完整样例：[examples/sdk-create-plan.ts](../examples/sdk-create-plan.ts)

### subscribeToEvents — event-driven sync (< 15s vs 2min polling)

```ts
import { subscribeToEvents } from '@agentxv2/sdk'

const unwatch = await subscribeToEvents(publicClient, {
  identityRegistryAddress,
  subscriptionManagerAddress,
  events: ['Transfer', 'AgentRegistered', 'PlanCreated', 'Subscribed'],
  fromBlock: 123456,
  onEvent: ({ type, args, txHash }) => {
    if (type === 'AgentRegistered') syncAgent(Number(args.agentId))
  },
})
// ... later: unwatch()
```

---

## A2A Daemon — Multi-Agent Interop

```ts
import { A2ADaemon } from '@agentxv2/sdk/agent-loop'

const a2a = new A2AProtocol({
  contractAddress: '0x7F42a7dC4A0F3C107664C3750bE1B5B6fa6BEb86',
  publicClient,
  walletClient,
})

// Start daemon — auto-processes incoming A2A tasks
const daemon = new A2ADaemon({
  agentId: 53,
  a2a,
  gatewayUrl: 'https://agentx.0xainet.top',   // 生产域名；本地开发可填 http://localhost:3090
  pollIntervalMs: 15000,
  autoComplete: true,
})

daemon.on('taskCompleted', (result) => {
  console.log(`Task #${result.task.taskId} completed!`, result.txHash)
})

daemon.start()
// daemon.stop()
```

**Flow:**
```
Agent A → createTask(Agent B) on-chain
Gateway Worker → detects → LLM processes → stores result
SDK A2A Daemon → polls Gateway → gets result → completeTask() on-chain
```

---

## Architecture

```
┌─────────────────────────────────────────────────────────────┐
│                      @agentxv2/sdk                           │
├──────────┬──────────┬──────────┬───────────────────────────┤
│  Core    │  Agent   │ AgentLoop│  React                    │
│ crypto   │ Runner   │ executor │  useAgentRunner           │
│ types    │ useAgent │ loop     │                           │
├──────────┼──────────┼──────────┼───────────────────────────┤
│ Registry │ Subscrip │ A2A      │ Reputation                │
│ register │ subscribe│ protocol │ giveFeedback              │
│ query    │ verify   │ daemon   │                           │
├──────────┼──────────┼──────────┼───────────────────────────┤
│ MCP      │ IPFS     │ LLM      │ Config                    │
│ Connector│ Uploader │ Factory  │ Chains                    │
│          │          │ OpenAI   │                           │
│          │          │ Gateway  │                           │
├──────────┼──────────┼──────────┼───────────────────────────┤
│ Endpoint │ConfigReg │ Payment  │ AgentWallet               │
│ MultiEP  │ KV Store │ Gateway  │                           │
└──────────┴──────────┴──────────┴───────────────────────────┘
```

---

## API Reference

### Main Exports

| Export | Module | Description |
|--------|--------|-------------|
| `AgentRunner` | agent | Decrypt + load Agent context from chain |
| `AgentLoop` | agent-loop | ReAct engine: Think → Tools → Observe → Repeat |
| `OpenAIProvider` | llm | Direct LLM provider with SSE streaming |
| `GatewayProvider` | llm | Multi-tenant SaaS LLM via AgentX Gateway |
| `createLLMProvider` | llm | Auto-select provider based on config |
| `MCPConnector` | mcp | MCP tool discovery + remote execution |
| `AgentRegistry` | registry | Register and query agents on-chain |
| `SubscriptionManager` | subscription | Subscribe (ETH/ERC20), verify, cancel, trial |
| `subscribeToEvents` | events | Contract event stream (Transfer/AgentRegistered/PlanCreated/Subscribed) |
| `AgentX402` | subscription | Auto-subscription gate + X402 payment |
| `A2AProtocol` | a2a | Agent-to-Agent task delegation |
| `A2ADaemon` | agent-loop | Background daemon for auto-processing A2A tasks |
| `ReputationRegistry` | reputation | Feedback + reputation queries |
| `ConfigurationRegistry` | configuration | On-chain KV configuration |
| `MultiEndpointClient` | endpoint | Multi-endpoint routing |
| `IPFSUploader` | ipfs | Upload to IPFS via Pinata or custom endpoint |
| `publishAgent` | core | Full encrypt + IPFS upload + pack pipeline |
| `KNOWN_CHAINS` | config | Pre-configured chain configs |

### Crypto Exports (from `@agentxv2/sdk/core`)

| Export | Description |
|--------|-------------|
| `aesEncrypt(plaintext, keyHex)` | AES-256-GCM encrypt → base64 |
| `aesDecrypt(ciphertext, keyHex)` | AES-256-GCM decrypt |
| `generateAesKey()` | Generate random 256-bit AES key (hex) |
| `eciesEncrypt(dataHex, publicKey)` | ECIES encrypt with secp256k1 public key |
| `eciesDecrypt(dataHex, privateKey)` | ECIES decrypt with secp256k1 private key |
| `encryptPayload(payload, pubKey)` | One-shot: AES encrypt + ECIES wrap |
| `decryptPayload(encrypted, privKey)` | One-shot: ECIES unwrap + AES decrypt |
| `packAgentForPublish(payload, pubKey)` | Package agent for on-chain registration |
| `publishAgent(config)` | Full pipeline: encrypt + IPFS upload |
| `generateKeyPair()` | Generate secp256k1 key pair |
| `getPublicKey(privateKey)` | Derive public key from private key |
| `randomBytes(length)` | CSPRNG random bytes (cross-runtime) |

### Sub-path Imports

| Path | Description |
|------|-------------|
| `@agentxv2/sdk` | All modules (main entry) |
| `@agentxv2/sdk/core` | Types, crypto (AES-256-GCM + ECIES) |
| `@agentxv2/sdk/react` | `useAgentRunner` React hook |
| `@agentxv2/sdk/agent-loop` | AgentLoop, executor, tool builder, A2A daemon |
| `@agentxv2/sdk/llm` | OpenAIProvider, GatewayProvider, factory |
| `@agentxv2/sdk/endpoint` | MultiEndpointClient |
| `@agentxv2/sdk/configuration` | ConfigurationClient |
| `@agentxv2/sdk/ipfs` | IPFSUploader (Pinata + custom endpoint upload) |
| `@agentxv2/sdk/memory` | MemoryProvider interface + types (v0.6.9) |
| `@agentxv2/sdk/traces` | TraceEmitter interface + types (v0.6.9) |
| `@agentxv2/sdk/skills` | Browser control skill utilities (v0.6.9) |
| `@agentxv2/sdk/conversation` | ConversationClient — remote Conversation Service client (v0.7.0) |

---

## Encryption Pipeline

```
Publisher creates Agent:
  AgentPayload → AES-256-GCM encrypt → IPFS (CID)
  AES key → ECIES wrap → on-chain NFT metadata
  Mint Agent NFT via IdentityRegistry

Subscriber uses Agent:
  Verify subscription (SubscriptionManager)
  Fetch encrypted payload from IPFS
  Read ECIES-wrapped key from on-chain NFT
  Decrypt → { prompt, skills, mcp }
  skills[n].execute() → Open (local) or MCP (remote with ECDSA auth)
```

---

## Supported Chains

| Network | Chain ID | RPC | Gas Token |
|---------|----------|-----|-----------|
| **OxaChain L1** | **19505** | `https://rpc-oxa.0xainet.top` | OXA |
| Sepolia (Testnet) | 11155111 | `https://ethereum-sepolia-rpc.publicnode.com` | ETH |

---

## On-Chain Contracts

### OxaChain L1 (Mainnet)

| Contract | Address |
|----------|---------|
| IdentityRegistry | `0xbf5F9db266c8c97E3334466C88597Eb758AfE212` |
| SubscriptionManager v3 | `0x019AC9d945467478Dd371CDbD70cb2f325800E6B` |
| A2AProtocolRegistry v2 | `0x7F42a7dC4A0F3C107664C3750bE1B5B6fa6BEb86` |
| ReputationRegistry | `0x6a18C2664E1b42063860d864b6448b824d7B843F` |
| ConfigurationRegistry | `0x07280674ccc2898Fd038A9e3C22005CA83ffD2F8` |
| MultiEndpointRegistry | `0xB361d04F49000013FC131D3C59C41c8486C64f8c` |

### Sepolia (Testnet)

| Contract | Address |
|----------|---------|
| IdentityRegistry | `0xe94ad380d3F8d08a7590eda0C84f354a93F96e5F` |
| SubscriptionManager v3 | `0xC15fE80b9d800abb72121F353a6ae6d6E9077E63` |
| A2AProtocolRegistry v2 | `0x309C7447d89f3087A9924BB686d88df020F7e9cB` |
| ReputationRegistry | `0xeb6B410ea71b8d9dA0c96f6A91d35027CE143DC9` |
| ConfigurationRegistry | `0x68DcE00e4C9077c94BC68016cD14B09557faEA6c` |
| MultiEndpointRegistry | `0xEB5e866f186d4B73F97aa0d70B86f2C6e2e21Cb7` |

---

## Gateway Integration

For multi-tenant SaaS deployments, the Gateway package (`@agentxv2/gateway`) provides:

```
npm install @agentxv2/gateway@0.1.2
```

Features: wallet-based auth (EIP-191 + JWT), rate limiting (IP + tenant), LLM proxy (OpenAI/DeepSeek), MCP server, A2A background worker, admin dashboard API, PostgreSQL + Redis persistence.

Configuration: 26 environment variables — see `gateway/.env.example`.

---

## Payment Method Decision Guide (v0.11.7)

AgentX exposes several payment rails. Pick by **what is being paid for**, **who pays**, and **whether a wallet is present**:

| Payment | What it pays for | Payer | Wallet? | Best for | SDK entry |
|---|---|---|---|---|---|
| `chain` | subscribing to a paid Agent (on-chain escrow) | end user | yes | crypto-native users; auditable on-chain subscription; publisher gets 97.5% | `SubscriptionPayments.pay({ method: 'chain' })` |
| `fiat` | subscribing to a paid Agent (Stripe card) | end user | no | Web2 users without a wallet; credit-card checkout | `SubscriptionPayments.pay({ method: 'fiat' })` → `sessionUrl` |
| `x402` | subscribing **or** pay-per-call (native-token balance) | end user (balance) | yes (only to fund) | frictionless per-call / auto-funding; server-side A2A pay-per-call (R19.7) | `X402Client` · `BillingClient` pre-check |
| `tenant-plan` | platform subscription tier (daily quota / rate limits) | B-end tenant | yes (chain/x402) or no (fiat) | B-end buying platform capabilities, **not** Agent access | `TenantPlanPayments.buy()` |
| `mpp` | high-frequency batched payments via a payment channel | payer | yes | repeated micro-payments without per-tx gas | `MPPClient` |
| `period` | pre-authorized N periods, charged later without re-signing | payer | yes | recurring billing / subscription periods | `PeriodClient` |
| `a2a` (on-chain) | auditable multi-agent delegation (task + gas) | user's wallet | yes | cross-org delegation, settlement, reputation | `A2AProtocol` + `onchain_approval_required` SSE |

**Decision rules**

- Subscribing to a paid Agent → `chain` (wallet) / `fiat` (no wallet) / `x402` (auto-funded); `hasAccess()` unifies the check (chain **OR** fiat/x402).
- Buying platform quota/tier as a B-end tenant → `TenantPlanPayments`.
- Pay-per-call delegation → pre-check with `BillingClient` (`endUserId` wallet), then let the Gateway auto-deduct from the x402 balance (server-side, R19.7 — zero caller changes).
- Auditable / settled delegation → on-chain `A2AProtocol` (the user's wallet signs `createTask` and pays the gas; the platform never holds a signing key).

**Frontend adaptation**

- Connect a wallet via wagmi + viem (`oxaChain`, chainId 19505) for the `chain` / `x402` / `a2a` rails; `fiat` only needs a redirect (`window.location.assign(sessionUrl)`).
- Renewal UI (three-rail picker): `SubscriptionPayments.pay({ method })` → `fiat` returns `sessionUrl` to redirect; `x402`/`chain` return a `txHash` to verify + refresh access.
- Pay-per-call: `BillingClient.getBalance({ endUserId })` before delegating; if `balanceWei < priceWei`, prompt a top-up to `payTo`.
- On-chain approval: on SSE `onchain_approval_required`, show a wallet modal so the **user** signs `createTask` (they pay gas) — see the frontend `OnchainApprovalModal`.
- Runnable B-end examples (Chinese): [docs/sdk-integration-example.md](../docs/sdk-integration-example.md).

## Multi-Rail Subscription Payments (v0.9.4)

`SubscriptionPayments` is the single entry point for subscribing across every AgentX payment rail — chain (on-chain escrow), fiat (Stripe card via the Gateway) and x402 (native-token period payment). `fiat` / `x402` / `hasAccess()` go through the unified `/api/v1/payments` endpoint (the `@0xinfrax/payments` engine); `chain` works fully off-Gateway.

```ts
import { SubscriptionManager, SubscriptionPayments } from '@agentxv2/sdk'

const sm = new SubscriptionManager({ contractAddress, publicClient, walletClient })
const payments = new SubscriptionPayments({
  gatewayUrl: 'https://gw.example.com', // required for fiat / x402 rails
  subscriptionManager: sm,              // required for chain rail & x402 auto-funding
  walletClient,
  chain: 'oxachain',
})

await payments.pay({ method: 'chain', planId: 1, agentId: 3 })            // on-chain escrow
const { sessionUrl } = await payments.pay({                               // Stripe redirect
  method: 'fiat', planId: 1, agentId: 3, subscriber: '0xabc',
})                                                                        // amount auto-priced from plan
await payments.pay({ method: 'x402', planId: 1, agentId: 3, subscriber: '0xabc' }) // auto-funded native payment
const ok = await payments.hasAccess(3, '0xabc')                           // unified chain-OR-fiat/x402 check
```

- `pay({ method })` returns a discriminated result: `{ method: 'chain', subscriptionId, txHash }` / `{ method: 'fiat', sessionUrl, sessionId, redirect: true }` / `{ method: 'x402', subscriptionId, txHash, creditedWei }`.
- For `fiat`, `amountCents` is optional — the Gateway derives the USD amount from the on-chain plan price (`FIAT_TOKEN_USD_PRICE`). Supply `amountCents` to override.
- For `x402` without `txHash`, the payment is sent automatically from `walletClient` (max of plan price / protocol price), then verified & registered by the Gateway.

### TenantPlanPayments (v0.11.5) — platform subscription-tier purchases

Since **0.11.5** (R19.3) integrators can purchase a platform subscription tier (tenant plan) through the same unified payments endpoint. The business binding is `purpose='tenant-plan'` + `tenantPlanId` — the Gateway verifies the on-chain payment (chain / x402 rails) or Stripe checkout (fiat), then binds the plan to the tenant (quota / rate limits / platform models).

```ts
import { TenantPlanPayments } from '@agentxv2/sdk'

const plans = new TenantPlanPayments({
  gatewayUrl: 'https://gw.example.com', // required for fiat / x402 rails
  chain: 'oxachain',
})

// chain / x402 rail — supply the on-chain payment tx, then the plan is bound
const { tenantId, planId, planSlug, quotaDaily } = await plans.buy({
  method: 'x402', tenantPlanId: '...', subscriber: '0xabc', txHash: '0x...',
})

// fiat rail — returns a Stripe checkout URL (webhook binds on completion)
const { sessionUrl } = await plans.buy({
  method: 'fiat', tenantPlanId: '...', subscriber: '0xabc',
  successUrl: 'https://app.example.com/b/success', cancelUrl: 'https://app.example.com/b',
})
```

- `buy({ method })` returns a discriminated result: `{ method: 'fiat', sessionUrl, sessionId, redirect: true }` / `{ method: 'chain' | 'x402', tenantId, planId, planSlug, quotaDaily, txHash }`.
- **chain / x402 必须携带 `txHash`**（先由调用方发起链上支付再提交）；fiat 无需 `txHash`，返回 Stripe checkout 跳转链接。
- `PaymentsClient.create({ purpose: 'tenant-plan', ... })` accepts the same `purpose` metadata for raw protocol-level purchases.
- Insufficient funds → the Gateway rejects with HTTP **422** before binding the plan.

### BillingClient (v0.11.6) — B-end balance pre-check

Since **0.11.6** (R19.7 companion) B-end integrators can query the x402 ledger balance **before** delegating to an unsubscribed agent — no more guessing whether the next pay-per-call will hit HTTP `403 AGENT_ACCESS_DENIED` (insufficient balance). Backed by `GET /api/v1/billing/balance` (read-only, idempotent, no side effects).

```ts
import { BillingClient } from '@agentxv2/sdk'

const billing = new BillingClient({
  gatewayUrl: 'https://gw.example.com',
  apiKey: 'agentx_xxx', // or accessToken
})

// tenant balance (default)
const { balance, balanceWei, currency, updatedAt, payTo, priceWei } = await billing.getBalance()

// per-end-user balance (partner callers may proxy an end-user's 0x wallet)
const userBalance = await billing.getBalance({ endUserId: '0xabc…' })

// pre-check before a pay-per-call delegation (R19.7)
if (priceWei && BigInt(balanceWei) < BigInt(priceWei)) {
  // insufficient — show a top-up prompt: send native token to `payTo`
}
```

- `getBalance()` never throws on a zero / never-funded balance — `balance` is `"0"` (normal response).
- `balance` is a high-precision OXA decimal string (`"1.500000000000000000"`); use `balanceWei` (raw wei string) for exact comparison against `priceWei`.
- `payTo` / `priceWei` are present only when x402 is enabled (the optional enhancement), so callers can build a funding prompt directly.
- Auth: same as `ConversationClient` — `X-Api-Key` (tenant key) or Bearer JWT. `401` on missing/invalid auth; `403` on a suspended account.
- **Legacy partner tenants keyed by a logical name** (e.g. `partner-xxx`) hold no on-ledger balance — the pay-per-call ledger is keyed by the **end user's `0x` wallet** that signs the delegation. For these callers the default (tenant) query returns `"0"`; always pre-check with `getBalance({ endUserId })` (the wallet the delegation will charge).

### CentralAgentClient (v0.11.11) — 中心化 Agent（C-6，链下市场）

Since **0.11.10** the SDK can pay for **centralized-agent** subscriptions — `SubscriptionPayments.pay({ method: 'x402', agentId, planId, priceWei, period })` uses the off-chain `central_plans` price (negative agentIds live in the Gateway DB, not on-chain). Since **0.11.11** the read side is wrapped in `CentralAgentClient` so third-party integrators (e.g. AItrader) don't need to hand-roll the Gateway HTTP calls:

```ts
import { CentralAgentClient } from '@agentxv2/sdk'

const central = new CentralAgentClient({ baseUrl: 'https://agentx.0xainet.top' })

// 查询中心化 Agent 列表（固定 source='central'，公开）
const { agents, total } = await central.list({ activeOnly: true, category: 'finance', page: 1, pageSize: 20 })

// 单个 Agent 详情 + 订阅计划（与链上同一结构，可直接喂给 SubscriptionPayments.pay）
const detail = await central.get(-3)
const plan = detail.subscriptionPlans![0]

// 订阅状态（resolveAccess 链下优先 —— 与 App 内订阅守卫同一判定）
const { active } = await central.checkSubscription('0xabc…', -3)

// 我的中心化订阅（fiat_subscriptions / x402 通道）
const { subscriptions } = await central.mySubscriptions('0xabc…')
```

```ts
// 中心化 Agent 周期订阅 —— 同一付费通道，priceWei 覆盖链下计划价格
const payments = new SubscriptionPayments({
  gatewayUrl: 'https://agentx.0xainet.top',
  subscriptionManager: sm, walletClient, chain: 'oxachain',
})
const result = await payments.pay({
  method: 'x402', subscriber: '0xabc…', agentId: -3, planId: plan.planId,
  period: 'month', priceWei: BigInt(plan.price),
})
```

- 所有读端点公开（无需鉴权）；发布（`POST /api/v1/agents/central`）需 JWT 或租户 Key，由调用方自持直调网关。
- 订阅付费走与链上 Agent 相同的 x402 通道，支付写入 `fiat_subscriptions` 后 `resolveAccess` 无感放行。

### A2A Delegated Tasks & Session Key Auto-pay (v0.11.8)

Since **0.11.8** (AItrader REQ-1/2/3) integrators can read the full **A2A delegated-task result** (incl. the `awaiting_payment` state + payment options), drive the **Session Key auto-pay** flow (user wallet authorizes an `x402-pay` session key against the engine; the Gateway auto-transfers from it when the x402 ledger balance is short), and query the Session Key authorization status — all through `ConversationClient`.

```ts
import { ConversationClient, A2A_STATUS } from '@agentxv2/sdk'

const client = new ConversationClient({
  gatewayUrl: 'https://gw.example.com',
  apiKey: 'agentx_xxx',               // or accessToken
  endUserId: '0xuser…',               // = the delegation's payment_payer (for retry)
})

// 1) task result — status 4 = awaiting_payment, carries payment_pay_options
const result = await client.getA2ATaskResult(7)
if (result.status === A2A_STATUS.AWAITING_PAYMENT && result.payment_pay_options?.sessionKey) {
  // render the "authorize Session Key" entry (prepay + sessionKey both offered)
}

// 2) engine capability info — end-user frontend reaches the engine's public
//    nonce / createSession endpoints directly (EIP-712-verified)
const info = await client.getSessionKeyInfo() // { enabled, chain, chainId, baseUrl }

// 3) after the user authorized + funded the session, re-activate the task
const { retried } = await client.retryA2ATask(7) // no ledger deduction; worker re-pays via session key

// 4) REQ-3: ledger balance + Session Key authorization status
const bal = await client.getBalance()
// bal.sessionKey = { enabled, authorized, sessionAddress?, maxTotal?, totalSpent?, validUntil? }
```

- `getA2ATaskResult` / `retryA2ATask` / `getSessionKeyInfo` hit the Gateway's `/api/v1/a2a/…` routes; `getBalance` hits `/api/v1/billing/balance` (same auth as `BillingClient`, but via `ConversationClient`).
- `retryA2ATask` requires the caller to be the task's `payment_payer` (`X-End-User-Id`) or an admin key — it re-activates a suspended task **without** deducting from the ledger; the worker re-runs and auto-pays through the session-key branch.
- If the Session Key engine is not configured (or escrow is enabled), `getSessionKeyInfo()` returns `{ enabled: false, … }` and `getBalance().sessionKey = { enabled: false, authorized: false }` — the feature degrades cleanly to the existing prepay path.

### Protocol Clients (v0.9.3)

Since **0.9.3** the SDK re-exports the generic engine's protocol clients from the root, so integrators can drive the P2-P4 rails directly against any Gateway deployment (AgentX-hosted or your own):

```ts
import { MPPClient, A2AClient, PeriodClient, X402Client, PaymentsClient } from '@agentxv2/sdk'

const base = { baseUrl: 'https://gw.example.com', accessToken: 'jwt...' } // accessToken optional

const mpp    = new MPPClient(base)      // payment channels
const a2a    = new A2AClient(base)      // two-phase paymentId (create → settle)
const period = new PeriodClient(base)   // period authorizations (charge / state)
const x402   = new X402Client(base)     // x402 v2 protocol (quote / pay / verify / balance)
const uni    = new PaymentsClient(base) // unified create / verify / access / info / quote
```

| Client | Endpoints | Typical flow |
|--------|-----------|--------------|
| `MPPClient` | `/api/v1/payments/mpp/{open,voucher,topup,settle,close,session}` | open a channel → submit signed cumulative vouchers (`voucher`, idempotent `mode: 'reuse'`) → auto-settle / close |
| `A2AClient` | `/api/v1/payments/a2a`, `/api/v1/payments/a2a/settle` | `create({ payer, amountWei })` → payer pays on-chain → `settle({ paymentId, txHash })` (idempotent) |
| `PeriodClient` | `/api/v1/payments/period/charge`, `/period/authorization` | one-time pre-authorization for N periods, then `charge(authorizationId)` per period — no re-signing |
| `X402Client` | `/api/v1/x402/{info,verify,balance}`, `quote`/`pay` (v2 headers) | `quote(url)` fetches the `PAYMENT-REQUIRED` challenge; `pay({ url, walletClient, account })` funds + signs + replays in one call |
| `PaymentsClient` | `/api/v1/payments` + `/verify` `/access` `/info` `/quote` | `create({ method: 'fiat'|'x402'|... })`, `verify(txHash)`, `access(subscriber, agentId)`, `info()` rails discovery |

> **Dependency note**: the clients come from [`@0xinfrax/payments`](https://www.npmjs.com/package/@0xinfrax/payments) `^0.1.3` (currently **0.1.3**, InfraX-maintained — the former AgentX-maintained `@agentxv2/payments` is deprecated), auto-installed as a dependency of `@agentxv2/sdk`. `PAYMENT_VERSION` (`'0.1.3'` — the aligned engine API version) is also exported from the SDK root. **Browser/bundler safe**: the engine uses only the Web Crypto API — no Node built-ins (`node:crypto` / `Buffer`) — so webpack/Next.js builds no longer fail on `UnhandledSchemeError`. Endpoints must be exposed by the Gateway the client points at (MPP/period/a2a routes exist on AgentX Gateway `/api/v1/payments/*`).

---

## Multi-Agent Orchestration Layering (v0.10.0)

Multi-agent delegation follows a **two-rail layering strategy** so integrators get real-time, zero-cost orchestration by default and only pay for on-chain guarantees when they need them:

| Rail | When to use | Cost | Guarantees |
|------|-------------|------|-----------|
| **off-chain** (default) | same-platform, high-frequency, real-time conversational delegation | zero (no on-chain writes) | result returns synchronously in the conversation channel |
| **on-chain** (opt-in) | cross-org, settlement / reconciliation, reputation accumulation, third-party verification | gas paid by the **user's wallet** + task tx | auditable A2A taskId, on-chain record, settlement & reputation hooks |

> **v0.10.0 gas model (2026-08-08):** on-chain rail costs are **never paid by the platform**. When the user explicitly requests an auditable / settled delegation, the Conversation Service emits an `onchain_approval_required` SSE event and the **user's own wallet** submits `createTask` — they pay the gas and become the on-chain `clientAddress` (the contract records `clientAddress = msg.sender`). The Gateway no longer holds any signing key (`A2A_WORKER_PRIVATE_KEY` removed) and never writes to the chain; sub-tasks created by the a2a-worker run **off-chain inline** (local negative pseudo taskIds), so only the top-level task the user signs is on-chain.

Inside a conversation run, the main agent is given two platform tools (injected by the Conversation Service, same access boundary as chat — only agents the caller owns or is subscribed to):

- `agentx_list_agents` — discover the agents the caller may delegate to (id / name / description / category).
- `agentx_delegate` — `{ targetAgentId, message, mode? }`. **Default `mode: "offchain"`**: the sub-agent runs synchronously inside the conversation channel and its final answer returns to the main agent in real time. **When the user explicitly requests an auditable / settled / on-chain delegation** (e.g. "上链", "可审计", "结算", "on-chain", "audit"), use `mode: "onchain"`: the service emits `onchain_approval_required` and the **user signs the A2A `createTask` in their own wallet** (they pay the gas); the returned `taskId` is the audit trail, picked up by the Gateway A2A worker and recorded in `a2a_task_results`.

Platform configuration (Conversation Service env):

```bash
ORCHESTRATE_TOKEN=...                 # must match the Gateway's ORCHESTRATE_TOKEN
ORCHESTRATE_DEFAULT_MODE=offchain     # default rail: offchain | onchain
ORCHESTRATE_MAX_DEPTH=4               # max nested delegation depth
```

> The SDK `A2AProtocol` / `A2ADaemon` remain the explicit on-chain rail for integrators who want settlement & reputation without the chat channel — same principle: the caller's own wallet signs `createTask`.

---

## Version History

| Version | Date | Highlights |
|---------|------|-----------|
| **0.11.9** | 2026-08-29 | **租户作用域权限下放（SK-1）** — `AgentWalletConfig` 新增可选 `apiKey` 模式：`{ baseUrl, apiKey }` 用租户 API Key 管理**自有 agent**（`agents.owner = 租户钱包`）的 MPC 钱包（bind/unlock/status/list/delete），越权 403；`{ baseUrl, adminKey }` 平台全量（原行为不变），同时提供时优先 `adminKey`。配套 Gateway `/api/v1/admin/agent-payers` 双轨鉴权已部署。**No breaking changes**，纯附加 |
| **0.11.8** | 2026-08-25 | **A2A 委派 Session Key 自动扣费（AItrader REQ-1/2/3）** — `ConversationClient` 新增 `getA2ATaskResult`（`a2a_task_results` 行，含 `status=4 awaiting_payment` + `payment_pay_options`）/ `retryA2ATask`（授权 Session Key 后重新激活挂起任务，不扣 ledger）/ `getSessionKeyInfo`（引擎能力信息）/ `getBalance`（x402 余额 + `sessionKey` 授权状态）；新增 `A2A_STATUS` 状态常量 + `A2ADelegatedTaskResult` / `SessionKeyInfo` / `SessionKeyAuthStatus` / `BillingBalance` 类型。端用户钱包 EIP-712 签名授权 `x402-pay` 会话后，Gateway 在余额不足时自动经引擎代签转账补足。**No breaking changes** |
| **0.11.7** | 2026-08-18 | **Agent 自主钱包 API + `@0xinfrax/payments` `0.1.4`** — 新增 `AgentWalletConfig`（`bindWallet` / `authorizePaymentSession`（邮箱验证码解锁）/ `status` / `list` / `unbind`）：绑定 agent 与 InfraX MPC 钱包（Email 2-of-2 TSS），A2A 委派按次付费由 gateway 服务端自动代付（agent-payer）；依赖升级 `0.1.3 → 0.1.4`（escrow 透传 / ERC20 deposit / 402 结构化错误），HTTP 契约与既有客户端签名不变（补 0.11.6 遗漏的 npm 发布）。**No breaking changes** |
| **0.11.6** | 2026-08-16 | **B 端余额预检（R19.7 companion）** — 新增 `BillingClient` + `GET /api/v1/billing/balance`：委派未订阅 agent 前程序化预检 x402 余额（租户维度默认，`endUserId` 0x 钱包透传返回端用户余额）；返回 `{ balance (OXA 高精度 decimal), balanceWei, currency, updatedAt, payTo?, priceWei? }`；余额 0 正常返回不报错；鉴权沿用 X-Api-Key / Bearer JWT。解决 aihunter-saas 提出的「先撞 403 再引导充值」体验问题。**No breaking changes** |
| **0.11.5** | 2026-08-12 | **R19.3/R19.7 商业化闭环** — 新增 `TenantPlanPayments`（平台套餐购买：统一支付入口 `purpose=tenant-plan`，chain/fiat/x402 三轨，`buy()` 返回 `{ method, ... }` 判别结果，绑定租户套餐）；服务端 A2A 按次付费（R19.7）——未订阅 agent 时按次扣 x402 余额（服务端 deduct + 审计幂等），SDK 调用方零改动、对话自动触发；`PaymentsClient.create({ purpose: 'tenant-plan' })` 支持套餐购买元数据。**No breaking changes** |
| **0.11.4** | 2026-08-11 | **B 端计费策略对齐（R18）** — 服务端「partner 任务强制 BYOK」已废除（2026-08-11 起不强制；未带 key 时走平台兜底 key，平台按 done 事件 usage 精确计费，扣套餐每日配额；`400 LLM_KEY_REQUIRED` 不再返回）；`ConversationSSEEvent` done 事件新增可选 `llmSource?: 'byok' | 'platform'`（计费来源标记，仅供观测）。SDK 调用方**零代码改动**（BYOK 参数仍可用），README/UPGRADE 文档同步。**No breaking changes** |
| **0.11.3** | 2026-08-10 | **R17.6 a2a/period 回归模块 rails** — 跟随 infraX `@0xinfrax/payments` `^0.1.3`（0.1.3 在模块内置恢复 a2a/period rails，并新增 batch/invite/transfer）；AgentX Gateway 自托管 a2a/period 迁移为模块委托，**HTTP 契约与客户端签名完全不变**，`PAYMENT_VERSION` 对齐 `0.1.3`。`A2AClient` 继续由 SDK 本地实现（修复 0.11.1 ESM 导入崩溃）。**No breaking changes** |
| **0.11.2** | 2026-08-10 | **A2AClient ESM 修复** — `A2AClient` 从 `@0xinfrax/payments` 导入（0.1.2 已移除该导出 → ESM 构建 `Named export not found` 崩溃）改为 SDK 本地实现，签名不变；`PAYMENT_VERSION` 对齐 `0.1.2`。CJS 调用方不受影响。**No breaking changes** |
| **0.11.1** | 2026-08-10 | **首次跟随演练（F2）** — 跟随 infraX `@0xinfrax/payments` `^0.1.1`（补丁版：新增 `createWebhookForwarder` 事件出站转发 + `ChainAdapter` `rpcHeaders`）；解耦回归 19 项断言通过（消费已安装 npm 包）、sdk 32/32 全绿；`PAYMENT_VERSION` 对齐 `0.1.1`。`SubscriptionPayments` + 协议客户端 API 不变。**No breaking changes** |
| **0.11.0** | 2026-08-10 | **Payment engine migrated** — the underlying engine moved from the AgentX-maintained `@agentxv2/payments` (now **deprecated**) to the InfraX-maintained [`@0xinfrax/payments`](https://www.npmjs.com/package/@0xinfrax/payments) `^0.1.0`; capabilities identical (chain / Stripe fiat / x402 v1+v2 / MPP channels / stablecoin EIP-3009+Permit2 / period authorizations / a2a-pay), dependency-source only; `PAYMENT_VERSION` aligned to `0.1.0`. `SubscriptionPayments` + protocol clients (MPP/A2A/Period/X402/Payments) API unchanged. **No breaking changes** (see UPGRADE.md) |
| **0.10.1** | 2026-08-08 | **Released** — per-request `endUserId` on `createTask` (body) / `stream` (header) for B-end subscription proxying; B-end key vs user JWT behavior documented (identical P9 capability bits; partner tasks **require BYOK** → `400 LLM_KEY_REQUIRED`; platform MCP & on-chain are JWT-only); MCP boundary note (platform `/mcp` vs self-hosted MCP). Non-breaking additive change (see UPGRADE.md) |
| **0.10.0** | 2026-08-08 | **Complete feature release** — consolidates the 0.9.x line into a stable baseline: typed `onchain_approval_required` SSE event + `OnChainApprovalRequest` (user-wallet-signed on-chain delegation, no platform gas), agent categories, unified payments rails, sessions & parallel tasks, streaming tool_call fix. **No breaking changes** |
| **0.9.6** | 2026-08-08 | **Typed on-chain approval event** — `ConversationSSEEvent` adds `'onchain_approval_required'` + `OnChainApprovalRequest { targetAgentId, taskType, inputData }` so consumers no longer need `as unknown as` narrowing when the agent requests an auditable on-chain A2A delegation (the user's wallet signs `createTask` and pays the gas). Frontend `useAgentChat` updated to the typed event (also resolves the pre-existing `AgentPayload.category` error). **No breaking changes** |
| **0.9.5** | 2026-08-08 | **Fix: streaming tool_call arguments dropped** — DeepSeek/OpenAI argument-delta chunks carry only an `index` (no `id`); `GatewayProvider` / `OpenAIProvider` now keep an `index→id` map so accumulated tool arguments attach to the real call (previously silently discarded). **No breaking changes** (see UPGRADE.md) |
| **0.9.4** | 2026-08-07 | **Agent application categories** — `AgentPayload.category` + `AGENT_CATEGORIES` / `AgentCategory` (13 enums); written to public metadata + on-chain attrs; `getAllAgents()` / `getAgentMetadata()` resolve `category`; Gateway `?category=` filter + byCategory aggregation; frontend Studio requires it, Marketplace categorizes by it. `@agentxv2/payments` resolved to **0.2.2** (ownership metadata). **No breaking changes** (see UPGRADE.md) |
| **0.9.3** | 2026-08-07 | **P2-P4 rails aligned** — `@agentxv2/payments` bumped to `^0.2.0` (MPP payment channels / stablecoin EIP-3009+Permit2 / period authorizations / a2a-pay); re-exports `MPPClient` / `A2AClient` / `PeriodClient` / `X402Client` / `PaymentsClient` from the SDK root. **No breaking changes** — `SubscriptionPayments` API unchanged (see UPGRADE.md) |
| **0.9.2** | 2026-08-07 | **Unified payments endpoint** — `SubscriptionPayments` fiat / x402 / `hasAccess()` now go through the Gateway's `/api/v1/payments` via the decoupled `@agentxv2/payments` engine (`^0.1.0`, new dependency); `fetchX402Info()` reads the rails-discovery `/info`. **No breaking changes** — `pay()` / `hasAccess()` / result types unchanged (see UPGRADE.md) |
| **0.9.0** | 2026-08-06 | Browser Control Skill extension: new actions `hover` / `press` / `select` (select+checkbox+radio) / `back` / `forward` / `getInfo` (url/title/viewport); `extractAccessibleDOM` now includes `name`/`role`/`aria-label`/input `value`/checkbox `checked`/anchor `target`; new `sleep(ms)` async pacing helper; `findElement` also matches `name` attr |
| **0.8.11** | 2026-08-07 | **Multi-rail subscription payments** — new `SubscriptionPayments` class (`pay({ method })` for `chain` / `fiat` / `x402`, `hasAccess()` unified chain-OR-fiat/x402 check, `fetchX402Info()` discovery); fiat `amountCents` now optional (Gateway auto-prices from the on-chain plan); x402 auto-funding from a configured `walletClient` |
| **0.8.10** | 2026-08-06 | Master-key crypto helpers `encryptWithKey()` / `decryptWithKey()` (AES-256-GCM, `base64(IV‖tag‖ciphertext)`, byte-compatible with Gateway at-rest key encryption); `parseTokenURIJSON` exported from the main entry; `A2AProtocol.createTask()` accepts raw string `input`; **subscription status mapping fix** (on-chain enum `0/1/2/3` → `pending/active/expired/cancelled`, previously shifted) |
| **0.8.9** | 2026-08-06 | Docs sync — Installation section points at v0.8.8 ("just install to use the new capabilities"); same code as 0.8.8 |
| **0.8.8** | 2026-08-06 | Docs sync — README updated for 0.8.7 (sessions & parallel tasks section) |
| **0.8.7** | 2026-08-06 | `ConversationClient` gains sessions & parallel tasks: `createSession()` / `createTask()` (returns `taskId` immediately, background execution) / `getTask()` / `listTasks()` / `cancelTask()` / `getCapabilities()`. New `ConversationTaskError` (`.status` / `.code`) — `createTask()` on a P9-disabled tenant/plan rejects with HTTP 403 `PARALLEL_TASKS_DISABLED`; used by the frontend parallel-task chat UI |
| **0.8.6** | 2026-08-06 | `ConversationChatParams` gains `tenantKeyId` — BYOK via a stored tenant-owned API key, resolved server-side by the Gateway (plaintext key never leaves the server); used by the new frontend own-key settings flow |
| **0.8.5** | 2026-08-06 | Docs sync — re-published with updated README (same code as 0.8.4) |
| **0.8.4** | 2026-08-06 | `ConversationClient` now supports Gateway JWT auth (`accessToken` → `Authorization: Bearer`, alternative to `apiKey`) and external abort (`stream(params, { signal })`); `tool_result` event gains optional `error` field. Frontend chat hook unified onto it (single SSE client implementation) |
| **0.8.3** | 2026-08-05 | Install fix: `wagmi` promoted from optional to required peer dependency — the package is now directly usable via `npm install @agentxv2/sdk@0.8.3` (no manual `wagmi` install); verified from a clean install (ESM + CJS, chain reads OK) |
| **0.8.2** | 2026-08-05 | Write-op fix: `createPlan()` / `subscribe()` / `releaseFunds()` / `cancel()` resolve the full viem `walletClient.account` instead of a bare address string — local/private-key signers now work (`eth_sendRawTransaction`); browser wallets unchanged. Verified on-chain (OxaChain L1) |
| **0.8.1** | 2026-08-04 | `parseTokenURIJSON()` fault-tolerant parsing aligned with Gateway indexer: base64 trailing garbage cleanup, unterminated JSON repair, regex fallback, explicit `ipfs://` handling |
| **0.8.0** | 2026-08-04 | Chain-data capabilities: `getAllAgents()` / `totalAgents()` / `getAgentMetadata()` on IdentityRegistry; `createPlan()` (typed period `day|week|month|year`) / `subscribe()` (event-parsed result) / `createPlanAndSubscribe()`; `subscribeToEvents()` event stream |
| **0.7.5** | 2026-08-04 | Fix AgentLoop forcing `ctx.model ?? 'gpt-4o'` over provider model — priority now `ctx.model ?? provider.model ?? default` |
| **0.7.4** | 2026-08-04 | `ConversationClient` adds `llmModel` (forwarded as `X-Llm-Model`) — BYOK now covers key + endpoint + model (e.g. `deepseek-v4-pro`) |
| **0.7.3** | 2026-08-04 | Stateless BYOK: `ConversationClient` adds `llmEndpoint` (forwarded as `X-Llm-Endpoint`) so callers supply their own LLM key + endpoint (e.g. DeepSeek) per request — no AgentX-side key storage needed |
| **0.7.2** | 2026-08-03 | Clarification interruption: `ConversationSSEEvent` adds `clarification` + `question`; `chat()` returns `result.clarification` when the service interrupts an ambiguous request |
| **0.7.1** | 2026-08-03 | ConversationClient inline mode: `prompt` + `skills` params (inject MCP/HTTP tools e.g. RAG), `agentId` now optional; Gateway forwards `X-Llm-Api-Key` |
| **0.7.0** | 2026-08-01 | **ConversationClient** (`@agentxv2/sdk/conversation`) — remote Conversation Service client: SSE streaming via Gateway, auto `X-Api-Key` / `X-End-User-Id` / `X-Llm-Api-Key`; Gateway Agent-as-MCP `tools/call` now executes skills directly (no LLM second-pass) |
| **0.6.9** | 2026-08-01 | Microservice Agent Conversation — 6-Phase Optimization: Conversation Service (Memory + Context + Sandbox), Observability (TraceEmitter), Skills Marketplace, Agent-as-MCP Export, Browser Control Skill, 3 new sub-path exports (memory/traces/skills) |
| **0.6.8** | 2026-07-28 | Fixed import paths in platform-tools (definitions.ts, executor.ts, index.ts) after module split; Frontend: 3 God Components modularized (AgentCardManager→5 files, AgentRegistration→4 files, RevenueDisplay→5 files) |
| **0.6.7** | 2026-07-27 | Code review: 22 fixes across contracts/gateway/frontend/sdk; Redis-backed auth; unified error handler; i18n agent dashboard; barrel exports; custom errors in SubscriptionManager; ValidationRegistry interface fix; TokenPriceOracle de-hardcoded |
| **0.6.6** | 2026-07-22 | A2A Worker + Daemon multi-agent interop, A2A tenant isolation, i18n EN/繁體中文, completeTask ABI fix, getAgentTasks |
| **0.6.5** | 2026-07-21 | Admin dashboard, A2A L1 redeploy, DeepSeek platform key, auth case-insensitive fix |
| **0.6.4** | 2026-07-20 | IPFSUploader (Pinata + custom endpoint), publishAgent pipeline, IPFS platform tools |
| 0.6.3 | 2026-07-19 | Production deploy, wallet auto-switch to OxaChain L1, MCP dual-chain fixes |
| 0.6.1 | 2026-07-15 | AgentLoop ReAct, OpenAIProvider, GatewayProvider, ToolExecutor |
| 0.5.4 | 2026-07-14 | MultiEndpointClient, ConfigurationClient, OxaChain L1 dual-chain |
| 0.2.0 | 2026-07-13 | AgentRunner, SubscriptionManager v3, A2A Protocol, MCP Connector |

---

## License

MIT
