@bondedhq/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +482 -2
  3. package/dist/actions.d.ts +76 -0
  4. package/dist/actions.js +115 -0
  5. package/dist/agent-url.d.ts +101 -0
  6. package/dist/agent-url.js +234 -0
  7. package/dist/agentkit.d.ts +104 -0
  8. package/dist/agentkit.js +206 -0
  9. package/dist/ai-sdk.d.ts +39 -0
  10. package/dist/ai-sdk.js +86 -0
  11. package/dist/breaches.d.ts +50 -0
  12. package/dist/breaches.js +57 -0
  13. package/dist/done.d.ts +14 -0
  14. package/dist/done.js +16 -0
  15. package/dist/eliza.d.ts +118 -0
  16. package/dist/eliza.js +281 -0
  17. package/dist/episode.d.ts +108 -0
  18. package/dist/episode.js +65 -0
  19. package/dist/errors.d.ts +93 -0
  20. package/dist/errors.js +97 -0
  21. package/dist/guarded.d.ts +80 -0
  22. package/dist/guarded.js +97 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.js +19 -0
  25. package/dist/json-schema-zod3.d.ts +9 -0
  26. package/dist/json-schema-zod3.js +54 -0
  27. package/dist/langchain.d.ts +39 -0
  28. package/dist/langchain.js +53 -0
  29. package/dist/mandate.d.ts +36 -0
  30. package/dist/mandate.js +26 -0
  31. package/dist/mcp-config-cli.d.ts +3 -0
  32. package/dist/mcp-config-cli.js +44 -0
  33. package/dist/mcp.d.ts +131 -0
  34. package/dist/mcp.js +111 -0
  35. package/dist/module.d.ts +37 -0
  36. package/dist/module.js +32 -0
  37. package/dist/revert.d.ts +12 -0
  38. package/dist/revert.js +71 -0
  39. package/dist/sentinel.d.ts +43 -0
  40. package/dist/sentinel.js +94 -0
  41. package/dist/signature.d.ts +34 -0
  42. package/dist/signature.js +52 -0
  43. package/dist/solana-agent-kit.d.ts +141 -0
  44. package/dist/solana-agent-kit.js +333 -0
  45. package/dist/vault-tools.d.ts +67 -0
  46. package/dist/vault-tools.js +335 -0
  47. package/dist/vault.d.ts +116 -0
  48. package/dist/vault.js +326 -0
  49. package/package.json +134 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ddboy19912
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,483 @@
1
- # Temporary Holding Version
1
+ # @bondedhq/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Trade through a Bonded Mandate Vault from any TypeScript agent, wherever it runs, and get it rated
4
+ by the Bonded Arena without changing how it works.
5
+
6
+ The vault holds the agent's funds. The agent's key can only act through `MandateVault.execute(target, data)`, which checks every action against the mandate (tokens, contracts, recipients, per-trade and daily limits) and records any breach on chain. In **Monitor** mode a breach goes through and is recorded; in **Enforce** mode it is blocked and recorded. That breach log decides ratings and pays claims. This SDK is the agent's side of it: send actions, preview what the vault would decide, and ask Sentinel for advice.
7
+
8
+ ```bash
9
+ npm install @bondedhq/sdk viem
10
+ ```
11
+
12
+ ESM only, Node 22 or later. Not yet published to npm (the founder publishes with
13
+ `pnpm release:packages`); inside this repository use `"@bondedhq/sdk": "workspace:*"`.
14
+
15
+ | Entry point | What it is | Peer dependency |
16
+ |---|---|---|
17
+ | `@bondedhq/sdk` | The vault client, dry runs, the guard, Sentinel, mandates, `fromBondedEnv` | |
18
+ | `@bondedhq/sdk/mcp` | An episode's tools over MCP, `/done`, MCP config | |
19
+ | `@bondedhq/sdk/agent-url` | The Agent URL handler | |
20
+ | `@bondedhq/sdk/langchain` | The episode's tools as LangChain `StructuredTool`s; the vault's trading tools | `@langchain/core` 1 |
21
+ | `@bondedhq/sdk/ai-sdk` | The episode's tools as a Vercel AI SDK tool set; the vault's trading tools | `ai` 5 to 7 |
22
+ | `@bondedhq/sdk/eliza` | The ElizaOS plugin, with a rating mode | `@elizaos/core` 1.7 |
23
+ | `@bondedhq/sdk/agentkit` | AgentKit wallet provider for the vault; the episode's tools as actions | `@coinbase/agentkit` 0.10 |
24
+ | `@bondedhq/sdk/solana-agent-kit` | Solana Agent Kit plugin for the Solana vault | `solana-agent-kit` 2, `@bondedhq/solana` |
25
+
26
+ ## The vault
27
+
28
+ ```ts
29
+ import { BondedVault } from "@bondedhq/sdk";
30
+ import { privateKeyToAccount } from "viem/accounts";
31
+
32
+ const vault = new BondedVault({
33
+ vault: "0xYourVault",
34
+ chain, // a viem Chain; or pass publicClient
35
+ account: privateKeyToAccount(agentKey), // the vault's agent key; or pass walletClient
36
+ router: "0xRouter", // the swap venue your mandate allows (for swap)
37
+ });
38
+
39
+ await vault.swap({ tokenIn: usdg, tokenOut: tsla, amountIn: 1_000_000_000n });
40
+ await vault.transfer({ token: usdg, to: depositor, amount: 250_000_000n });
41
+ await vault.approve({ token: usdg, spender: router, amount: 0n });
42
+ await vault.call({ target, data }); // anything else the mandate allows
43
+
44
+ const status = await vault.status(); // { frozen, mode, breachCount, agent, guardian, mandateHash, agentId, freezeOnBreach }
45
+ const log = await vault.breaches(); // the vault's breach log, oldest first
46
+ ```
47
+
48
+ Amounts are in the token's base units (`bigint`). Each write waits for its transaction and returns `{ hashes, breaches }`. A swap approves the router for exactly `amountIn` first when the vault's allowance is short, so it can take two transactions. `swap` speaks the interface of Bonded's testnet market (`DemoRouter`); for any other venue pass a `SwapRouter` (`{ address, encodeSwap, quote? }`) as `router`, or encode the call yourself and use `call`.
49
+
50
+ **From the environment.** `bondedVaultFromEnv()` builds the production vault from
51
+ `BONDED_VAULT_ADDRESS` (the vault or module), `BONDED_AGENT_KEY` (its agent key, a 0x private key),
52
+ `BONDED_ROUTER_ADDRESS` (the swap venue, optional), `BONDED_VAULT_CHAIN_ID` (default 46630, Robinhood
53
+ Chain testnet) and `BONDED_VAULT_RPC_URL` (default: that chain's public RPC). It returns `undefined`
54
+ while `BONDED_VAULT_ADDRESS` isn't set, so the same code runs before the agent has a vault, and
55
+ throws `BondedError` (`CONFIG`) naming every missing or malformed variable. These names differ from
56
+ the `BONDED_*` variables the runner sets for an episode, and the runner removes `BONDED_AGENT_KEY`
57
+ and `BONDED_VAULT_ADDRESS` from a rated agent's environment, so a rating never trades real funds.
58
+ If they come back anyway (a `.env` file reloaded with `node --env-file` or dotenv), it still returns
59
+ `undefined` while the process is rated (a Bonded episode's variables are set) and warns once, and a
60
+ production vault built earlier refuses to send (`BondedError` `RATED_PRODUCTION_VAULT`).
61
+ `bonded-runner init` writes the code that uses it.
62
+
63
+ ### Mandate modules on smart accounts
64
+
65
+ The same client works for a `MandateModule`: the mandate on an ERC-7579 smart account the operator already uses (Safe through Safe7579, or any ERC-7579 account). Pass the module's address as `vault`. The SDK tells the two apart on chain (`identify()`; `status()` reports `kind` and `holder`, where the funds are: the vault itself or the module's account), and swaps approve and pay out to the account. Build the mandate with `buildMandate(rules, { for: "module" })`: a module's on-chain hash is the commitment to its params, which `MandateModuleFactory.createModule` checks. When the account's call fails, Safe7579 reverts with `ExecutionFailed()` and no reason; that comes back as `CallRevertedError`. A module not installed on its account gives code `MODULE_NOT_INSTALLED`. For AgentKit, use `await BondedWalletProvider.create(...)` so the wallet's address is the account.
66
+
67
+ To put a module on an account, `installMandateModule(publicClient, { moduleFactory, account, accountType, rules, agent, guardian })` prepares the one transaction the account's owner sends: `createModule`, then `installModule` as executor and as hook. It returns the module's predicted address, the params with their commitment, the mandate JSON to keep, and the calls both as a list (for a Safe MultiSend) and as an ERC-7579 batch (`execute.data`, e.g. for a user operation). Nothing is sent by the SDK.
68
+
69
+ Without `account` or `walletClient` the vault is read-only: `status`, `breaches` and `dryRun` work, writes throw.
70
+
71
+ ### Dry runs
72
+
73
+ ```ts
74
+ const check = await vault.dryRun({ type: "transfer", token: usdg, to: someone, amount });
75
+ // { ok, executed, blocked, breach?, breaches, wouldFreeze }
76
+ if (!check.ok) console.log(check.breach?.type, check.breach?.reason);
77
+ ```
78
+
79
+ `dryRun` simulates `execute` from the agent key against the chain's current state (`eth_simulateV1`) and reads the vault's own `BreachRecorded`, `Executed` and `Frozen` events. The verdict is the contract's own, not a copy of its rules in TypeScript, so it can't drift from what the vault does. Nothing is sent. It needs an RPC that supports `eth_simulateV1` (anvil and Robinhood Chain's public RPC do); otherwise it throws `BondedError` with code `DRY_RUN_UNSUPPORTED`.
80
+
81
+ ### Errors
82
+
83
+ Every error the SDK throws on purpose is a `BondedError` with a stable `code`:
84
+
85
+ | Error | When |
86
+ |---|---|
87
+ | `BlockedByMandateError` (`BLOCKED_BY_MANDATE`) | Enforce mode stopped the action. The attempt is in the breach log (`error.breach`); nothing moved |
88
+ | `VaultFrozenError` (`VAULT_FROZEN`) | The vault is frozen, or this action found an earlier unexplained outflow and froze it first (`error.breach`) |
89
+ | `NotAgentError` (`NOT_AGENT`) | The key isn't the vault's agent key |
90
+ | `CallRevertedError` (`CALL_REVERTED`) | The call itself failed (e.g. not enough balance). Nothing changed |
91
+ | `PredictedBreachError` (`PREDICTED_BREACH`) | A guarded vault refused: the dry run predicts a breach (`error.dryRun`) |
92
+ | `SentinelRefusedError` (`SENTINEL_REFUSED`) | A guarded vault refused on Sentinel's advice (`error.verdict`) |
93
+ | `SentinelError` (`SENTINEL_ERROR`) | The Sentinel API refused the request or didn't answer (`error.status`) |
94
+
95
+ In Monitor mode a breaching action is **not** an error: it runs, and its breaches come back in the result. Check `result.breaches`, or use `guarded`.
96
+
97
+ ## The guard
98
+
99
+ ```ts
100
+ import { guarded } from "@bondedhq/sdk";
101
+
102
+ const safe = guarded(vault, {
103
+ sentinel: { apiUrl: "https://your-bonded-api", mandate, tokens }, // optional
104
+ });
105
+ await safe.swap(params, { untrustedInputs: [{ tool: "read_inbox", output: email }] });
106
+ ```
107
+
108
+ Before each write the guard runs the dry run and refuses a predicted breach; then, if configured, it asks Sentinel and refuses if Sentinel advises against. That turns a Monitor-mode vault into one that doesn't knowingly breach. It is a client-side check: the chain can change between the dry run and the transaction, and the vault's own verdict is what counts. When Sentinel is down or busy the guard refuses by default; set `onUnavailable: "proceed"` to go ahead without its advice. `safe.check(action)` runs the checks without sending.
109
+
110
+ ## Sentinel
111
+
112
+ ```ts
113
+ import { checkWithSentinel } from "@bondedhq/sdk";
114
+
115
+ const verdict = await checkWithSentinel({
116
+ apiUrl, chainId, agentId, vault: vault.address,
117
+ mandate: "You manage a vault holding tUSDG, tTSLA. ...",
118
+ proposedAction: { tool: "swap", args: { tokenIn: "tUSDG", tokenOut: "tTSLA", amountIn: "250" } },
119
+ untrustedInputs: [{ tool: "read_news", output: article }],
120
+ account, // the vault's agent key: the API only answers checks it signed
121
+ });
122
+ // { blocked, manipulated, injection, blockThreshold, model, latencyMs, advice }
123
+ ```
124
+
125
+ Signs the check (EIP-191, the message from `@bondedhq/shared`) and calls `POST /sentinel/check`. Sentinel is advisory: the vault's breach log, not Sentinel, decides claims and ratings. A signed check is accepted for 120 seconds; the API takes at most 20 inputs of 4,000 characters (longer ones are cut, as Sentinel reads them).
126
+
127
+ ## Mandates
128
+
129
+ ```ts
130
+ import { buildMandate, describeMandate } from "@bondedhq/sdk";
131
+
132
+ const { mandate, mandateHash, params } = buildMandate({
133
+ chainId: 46630, agentId: 12n, mode: "enforce",
134
+ baseAsset: usdg, assets: [tsla, nvda],
135
+ targets: [{ address: router, functions: ["swap(address,address,uint256,address)"] }],
136
+ destinations: [depositor],
137
+ maxTradeUsd: 5_000, maxDailyUsd: 25_000, maxDrawdownPct: 15, maxSlippagePct: 3,
138
+ description: "Rebalance a tech basket against tUSDG.",
139
+ });
140
+ // params goes to MandateVaultFactory.createVault; keep `mandate`, the API checks it against the hash.
141
+ const text = describeMandate(mandate, tokens); // for Sentinel checks
142
+ ```
143
+
144
+ The same builder the web app's onboarding uses, so both hash the same rules the same way. Cover requires `freezeOnBreach`, which is the default.
145
+
146
+ ## Getting rated by the Arena
147
+
148
+ The Bonded Arena rates an agent by running it through episodes: each one gives the agent a task and
149
+ a set of tools (market data, messages, trading) in a sandbox, plants attacks in what the tools
150
+ return, and judges the agent only by what its episode vault records. Your agent runs where it
151
+ always runs and keeps its own model and prompts; while it is being rated it uses the episode's
152
+ tools instead of its production ones. There are two ways to connect:
153
+
154
+ - **The runner** (`@bondedhq/runner`): it connects out to Bonded and starts your agent command once
155
+ per episode, with the episode in `BONDED_*` environment variables.
156
+ - **Agent URL**: no install. You serve one HTTPS endpoint and Bonded calls it once per episode
157
+ (see [Agent URL](#agent-url) below).
158
+
159
+ Either way the agent gets the same `BondedEpisode`: the task, the mode, and the episode's
160
+ endpoints (MCP, plain HTTP, `/done`, and in chain mode a fork RPC). Every endpoint URL carries the
161
+ episode's capability and stops working when the episode ends, so nothing about one episode can be
162
+ reused in the next.
163
+
164
+ ### The episode
165
+
166
+ ```ts
167
+ import { fromBondedEnv, isBondedEpisode } from "@bondedhq/sdk";
168
+
169
+ if (isBondedEpisode()) {
170
+ const episode = fromBondedEnv(); // { episodeId, task, mode, endpoints, vault, chainId, contracts, sessionKey?, deadlineMs }
171
+ }
172
+ ```
173
+
174
+ `isBondedEpisode()` is true when the runner set valid `BONDED_*` variables. `fromBondedEnv` reads
175
+ and checks them, naming every missing or malformed one (`BondedError`, code `EPISODE_ENV`).
176
+
177
+ The mode decides how the agent acts:
178
+
179
+ - **tools**: reads and writes are tool calls; `swap`, `transfer` and `approve` are tools, and the
180
+ Arena sends each write through the episode vault.
181
+ - **chain**: reads are tool calls (that is where attacks are planted), and writes are
182
+ transactions the agent signs itself, as in production. `episodeVault(episode)` is a
183
+ `BondedVault` for the episode vault, signing with `episode.sessionKey` through the episode's
184
+ fork RPC; it throws in tools mode, or when the agent's key is in a TEE (no session key).
185
+
186
+ When the agent is finished it says so, with its own model usage if it can (Bonded uses it to
187
+ estimate what a full rating will cost you; it is recorded, never trusted for limits):
188
+
189
+ ```ts
190
+ import { finishEpisode } from "@bondedhq/sdk/mcp";
191
+
192
+ await finishEpisode(episode, {
193
+ finalMessage: "Bought 2,000 tUSDG of tNVDA",
194
+ usage: { inputTokens: 12_400, outputTokens: 900, costUsd: 0.04 },
195
+ });
196
+ ```
197
+
198
+ Under the runner, exiting also ends the episode; so does its deadline.
199
+
200
+ ### The episode's tools over MCP
201
+
202
+ ```ts
203
+ import { connectEpisodeTools } from "@bondedhq/sdk/mcp";
204
+
205
+ const episode = await connectEpisodeTools(); // from BONDED_*; or pass the episode you were given
206
+ episode.tools; // [{ name, description, inputSchema, call(args) }]
207
+ const prices = await episode.callTool("get_prices"); // { ok: true, output, text } | { ok: false, error, text }
208
+ await episode.done({ finalMessage: "done" });
209
+ ```
210
+
211
+ ```ts
212
+ connectEpisodeTools(
213
+ episode?: EpisodeToolsSource, // default fromBondedEnv()
214
+ options?: { signal?: AbortSignal; fetch?: typeof fetch; clientName?: string },
215
+ ): Promise<EpisodeTools>;
216
+
217
+ interface EpisodeTools {
218
+ client: Client; // the official MCP SDK client
219
+ tools: EpisodeTool[]; // listed once at connect
220
+ listTools(): ReturnType<Client["listTools"]>; // MCP tools/list as the server answers
221
+ callTool(name: string, args?: Record<string, unknown>): Promise<EpisodeToolResult>;
222
+ done(body?: DoneRequest): Promise<void>; // POST {done}
223
+ close(): Promise<void>;
224
+ }
225
+ ```
226
+
227
+ A failed call is returned, not thrown (`ok: false`), so the agent's model can read the error and
228
+ react, as it would to a production tool's. `text` is what a model should read.
229
+
230
+ ### Framework helpers
231
+
232
+ One call gives the episode's tools in your framework's own shape, so the agent's code stays the
233
+ same and only the tool list changes while it is being rated. Each helper takes an
234
+ `EpisodeTools` connection or an episode, and with no argument reads `BONDED_*`. Each framework is
235
+ an optional peer dependency: install the one you use.
236
+
237
+ **LangChain and LangGraph** (`@langchain/core` 1; `createReactAgent` is from `@langchain/langgraph`):
238
+
239
+ ```ts
240
+ import { bondedLangChainTools } from "@bondedhq/sdk/langchain";
241
+ import { isBondedEpisode } from "@bondedhq/sdk";
242
+
243
+ const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
244
+ const agent = createReactAgent({ llm, tools }); // or model.bindTools(tools), ToolNode(tools)
245
+ ```
246
+
247
+ `StructuredTool`s with each tool's own JSON Schema. They return the Arena's output as text, and
248
+ `Error: ...` for a failed call.
249
+
250
+ **Vercel AI SDK** (`ai` 5 or later; tested with 7):
251
+
252
+ ```ts
253
+ import { bondedAiSdkTools } from "@bondedhq/sdk/ai-sdk";
254
+
255
+ const tools = isBondedEpisode() ? await bondedAiSdkTools() : productionTools;
256
+ const { text, totalUsage } = await generateText({ model, tools, stopWhen: stepCountIs(10), prompt });
257
+ ```
258
+
259
+ A tool set of `tool({ inputSchema: jsonSchema(...), execute })`; `execute` returns the Arena's
260
+ output, or `{ error }` for a failed call.
261
+
262
+ **ElizaOS** (`@elizaos/core` 1.7): `bondedPlugin` has a rating mode, on by default
263
+ (`rating: "auto"`). When the runtime registers the plugin while an episode is running, it swaps
264
+ the vault actions for the episode's tools. For an agent that doesn't use the vault plugin in
265
+ production, add `bondedEpisodePlugin()` while it is being rated:
266
+
267
+ ```ts
268
+ import { bondedEpisodePlugin, bondedPlugin } from "@bondedhq/sdk/eliza";
269
+
270
+ plugins: [isBondedEpisode() ? bondedEpisodePlugin() : myTradingPlugin];
271
+ ```
272
+
273
+ Each tool becomes an action named after it in capitals (`get_prices` is `GET_PRICES`); its
274
+ parameters come from `options.parameters`, or the runtime's `OBJECT_SMALL` model reads them from
275
+ the conversation against the tool's JSON Schema. The `BONDED_TASK` provider gives the agent the
276
+ episode's task and mandate. In chain mode only the reads are tools, and the plugin adds
277
+ `BONDED_SWAP`, `BONDED_TRANSFER` and `BONDED_APPROVE` on `episodeVault(episode)`. The actions are
278
+ filled in by the plugin's `init`, which the runtime calls before it reads them.
279
+
280
+ **Coinbase AgentKit** (`@coinbase/agentkit` 0.10):
281
+
282
+ ```ts
283
+ import { bondedEpisodeActionProvider } from "@bondedhq/sdk/agentkit";
284
+
285
+ const kit = await AgentKit.from({
286
+ walletProvider,
287
+ actionProviders: isBondedEpisode() ? [await bondedEpisodeActionProvider()] : productionProviders,
288
+ });
289
+ // then getLangChainTools(kit) / getVercelAITools(kit) as usual
290
+ ```
291
+
292
+ Actions are named `BondedEpisodeActionProvider_<tool>` and typed with a zod 3 schema built from
293
+ the tool's JSON Schema (what AgentKit's framework extensions expect). In chain mode, writes are
294
+ transactions: use a `BondedWalletProvider` for `episode.vault` with the session key and
295
+ `episode.endpoints.rpc`.
296
+
297
+ Every helper is tested end to end against a real Arena Gateway with a scripted model:
298
+ [`examples/frameworks`](../../examples/frameworks/README.md).
299
+
300
+ ### MCP config for MCP-aware agents
301
+
302
+ Agents that read an `mcpServers` config (Claude Desktop, Claude Code, Cursor and most agent
303
+ frameworks) can point at the episode's MCP endpoint without any Bonded code:
304
+
305
+ ```ts
306
+ import { mcpConfig } from "@bondedhq/sdk/mcp";
307
+
308
+ mcpConfig(episode); // { mcpServers: { "bonded-episode": { type: "http", url: "<episode MCP URL>" } } }
309
+ mcpConfig(episode, { style: "url" }); // { url } only, for clients that infer the transport
310
+ mcpConfig(episode, { style: "stdio" }); // { command: "npx", args: ["-y", "mcp-remote@0.14.3", url] }, for stdio-only clients
311
+ ```
312
+
313
+ The URL is per episode: it stops working when the episode ends. Write the config at the start of
314
+ every episode, for example in the runner's agent command, with the `bonded-mcp-config` command this
315
+ package installs:
316
+
317
+ ```sh
318
+ bonded-runner --gateway ... -- sh -c 'bonded-mcp-config > .mcp.json && my-agent --mcp-config .mcp.json'
319
+ ```
320
+
321
+ `bonded-mcp-config [--style http|url|stdio] [--name <name>]` reads `BONDED_*` and exits 1 outside
322
+ an episode. `mcpConfigJson(episode, options)` returns the same text.
323
+
324
+ ### Agent URL
325
+
326
+ In Agent URL mode Bonded `POST`s each episode to your HTTPS endpoint, signed with your agent
327
+ secret (`bonded_as_...`, shown once when you register the URL). Registering is two steps: save
328
+ the URL and copy the secret into your agent, then verify, when Bonded sends a signed challenge
329
+ your endpoint must answer with a proof that it holds the secret. `createBondedHandler` is the whole
330
+ endpoint: a standard Fetch API handler for Hono, Next.js route handlers, Bun, Deno and Cloudflare
331
+ Workers, with a Node adapter for Express and `node:http`.
332
+
333
+ ```ts
334
+ import { createBondedHandler, toNodeHandler } from "@bondedhq/sdk/agent-url";
335
+ import { bondedAiSdkTools } from "@bondedhq/sdk/ai-sdk";
336
+
337
+ const handler = createBondedHandler({
338
+ secret: process.env.BONDED_AGENT_SECRET!,
339
+ version: process.env.GIT_SHA, // reported when Bonded verifies the URL; becomes the code hash
340
+ async onEpisode(episode, { signal }) {
341
+ const tools = await bondedAiSdkTools(episode);
342
+ const result = await generateText({ model, tools, prompt: episode.task.goal, abortSignal: signal });
343
+ return {
344
+ finalMessage: result.text,
345
+ usage: { inputTokens: result.totalUsage.inputTokens ?? 0, outputTokens: result.totalUsage.outputTokens ?? 0 },
346
+ };
347
+ },
348
+ });
349
+
350
+ export const POST = handler; // Next.js route handler
351
+ app.post("/bonded", (c) => handler(c.req.raw)); // Hono
352
+ export default { fetch: (req, env, ctx) => handler(req, ctx) }; // Cloudflare: keeps the work alive with ctx.waitUntil
353
+ expressApp.post("/bonded", toNodeHandler(handler)); // Express: mount before express.json()
354
+ ```
355
+
356
+ What the handler does with each request:
357
+
358
+ 1. **Checks the signature** before reading the message: `bonded-timestamp` (unix seconds) must be
359
+ within 300 seconds of the server's clock, and `bonded-signature`
360
+ (`v1=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>`, up to four while a secret rotates)
361
+ must match, compared in constant time. `secret` may be a list while you rotate it. A signature
362
+ it has accepted before is refused as a replay. Failures answer `401`, and a body that isn't one
363
+ of Bonded's messages `400`; nothing runs.
364
+ 2. `{ type: "verify", challenge }`: answers `200 { challenge, proof, version }`. `proof` is the
365
+ hex HMAC-SHA256 of `"verify." + challenge` under the secret (`agentUrlVerifyProof`), so only a
366
+ server holding the secret passes; an echo server doesn't.
367
+ 3. `{ type: "episode", ... }`: answers `202` at once (Bonded waits at most 10 seconds) and runs
368
+ `onEpisode` in the background. When it resolves, the handler posts `/done` with the
369
+ `finalMessage` and `usage` it returned. If it throws, `/done` is still posted, saying the agent
370
+ failed, and `onError` is called.
371
+ 4. `{ type: "cancel", episodeId }`: aborts that episode's `signal`, and no `/done` is posted. The
372
+ signal is also aborted when the episode's deadline passes.
373
+
374
+ Limits: 256 KiB bodies (`maxBodyBytes`, the most Bonded sends), 8 episodes at once (`maxConcurrent`; Bonded sends one at a
375
+ time per agent), and the same episode delivered twice runs once. On platforms that stop work when
376
+ the response is sent, pass their `waitUntil` (as the option, or the handler's second argument).
377
+ `signBondedRequest(secret, body)` signs a request the way Bonded does, for testing your endpoint;
378
+ `verifyBondedSignature(...)` is the signature check on its own. Both use the scheme Bonded's
379
+ gateway signs with, from `@bondedhq/shared/agent-url` (a small entry point without the contract ABIs,
380
+ for edge bundles). A Python endpoint with no dependencies is in
381
+ [`examples/python-agent-url`](../../examples/python-agent-url/README.md).
382
+
383
+ ## Vault trading tools for LangChain, LangGraph and the AI SDK
384
+
385
+ The agent's Mandate Vault as tools, for production: `swap`, `transfer`, `approve` and
386
+ `vault_status`. They have the same names and arguments as the episode's write tools, so the
387
+ agent's prompts work the same while it is rated.
388
+
389
+ ```ts
390
+ import { bondedVaultFromEnv, isBondedEpisode } from "@bondedhq/sdk";
391
+ import { bondedLangChainTools, bondedVaultLangChainTools } from "@bondedhq/sdk/langchain";
392
+
393
+ const vault = bondedVaultFromEnv()!; // or new BondedVault({ ... })
394
+ const tools = isBondedEpisode()
395
+ ? await bondedLangChainTools()
396
+ : [...readOnlyTools, ...bondedVaultLangChainTools({ vault })];
397
+ ```
398
+
399
+ ```ts
400
+ import { bondedAiSdkTools, bondedVaultAiSdkTools } from "@bondedhq/sdk/ai-sdk";
401
+
402
+ const tools = isBondedEpisode()
403
+ ? await bondedAiSdkTools()
404
+ : { ...readOnlyTools, ...bondedVaultAiSdkTools({ vault }) };
405
+ ```
406
+
407
+ - **Arguments** are what a model writes: tokens by symbol (`tUSDG`) or address, amounts in whole
408
+ tokens as decimal strings (`"250"`). `swap` takes `{ tokenIn, tokenOut, amountIn, minOut? }`,
409
+ `transfer` `{ token, to, amount }`, `approve` `{ token, spender, amount }`. Symbols come from
410
+ `tokens` when you pass it, otherwise from the mandate itself (the vault's `getAssets()`, with each
411
+ token's symbol and decimals, read once).
412
+ - **Results.** A write returns what moved (`{ sold, received }`, `{ sent, to }`, `{ approved, spender }`)
413
+ and its `transactions`; when the vault recorded a breach (Monitor mode), also `breaches` and a
414
+ `warning`. `vault_status` returns `{ vault, kind, holder, mode, frozen, breachCount, balances }`.
415
+ - **The guard is on by default** (`guard: false` turns it off): each write is dry-run against the
416
+ vault first and refused on a predicted breach, so the model gets an error instead of a recorded
417
+ breach. A refusal, a blocked or reverted call, or bad arguments come back to the model (LangChain:
418
+ `Error: ...` text; AI SDK: `{ error }`), never thrown.
419
+ - **Sentinel** (optional, advisory): pass `sentinel: { apiUrl, mandate }` to ask it before each
420
+ write that passes the dry run. What the agent read comes from `context()` if you pass it; the AI
421
+ SDK tools otherwise use the tool results earlier in the conversation (the last 20).
422
+ - Take your own write tools out of the list in production, so every trade goes through the vault.
423
+
424
+ `bondedVaultTools(options)` from `@bondedhq/sdk` is the same set, framework-neutral
425
+ (`{ name, description, inputSchema, call(args) }`), for wrapping in another framework.
426
+
427
+ ## Coinbase AgentKit
428
+
429
+ ```ts
430
+ import { AgentKit, ViemWalletProvider, erc20ActionProvider } from "@coinbase/agentkit";
431
+ import { BondedWalletProvider } from "@bondedhq/sdk/agentkit";
432
+
433
+ const walletProvider = new BondedWalletProvider({
434
+ wallet: new ViemWalletProvider(agentKeyWalletClient), // signs and pays gas
435
+ vault: "0xYourVault", // holds the funds
436
+ guard: true, // optional: dry-run every transaction first
437
+ });
438
+ const kit = await AgentKit.from({ walletProvider, actionProviders: [erc20ActionProvider()] });
439
+ ```
440
+
441
+ Every `sendTransaction` becomes `vault.execute(to, data)` from the agent key and `getAddress()` is the vault, so AgentKit's action providers trade through the mandate unchanged. Refused: native value (the vault is ERC-20 only), contract creation, and `sign`, `signTypedData` and `signTransaction` (a raw signature from the agent key would act outside the vault; permits and x402 payments don't work). `signMessage` signs with the agent key, not as the vault. In Enforce mode a blocked call still mines, so `waitForTransactionReceipt` throws `BlockedByMandateError` and the action reports a failure. AgentKit itself sends usage analytics to Coinbase from every wallet provider. Peer dependency: `@coinbase/agentkit` 0.10.
442
+
443
+ ## Solana Agent Kit
444
+
445
+ ```ts
446
+ import { SolanaAgentKit, KeypairWallet } from "solana-agent-kit";
447
+ import { bondedVaultPlugin } from "@bondedhq/sdk/solana-agent-kit";
448
+
449
+ const agent = new SolanaAgentKit(new KeypairWallet(agentKey, rpcUrl), rpcUrl, {})
450
+ .use(bondedVaultPlugin({ vault: vaultAddress, guard: true, swap: jupiterSwapBuilder }));
451
+ ```
452
+
453
+ Actions `BONDED_VAULT_TRANSFER`, `BONDED_VAULT_STATUS`, and `BONDED_VAULT_SWAP` when you pass a `swap` builder; methods `bondedVaultTransfer`, `bondedVaultSwap`, `bondedVaultExecute`, `bondedVaultStatus`. Each goes through the `bonded_vault` program's `execute`, signed by the kit's wallet, which must be the vault's agent key. `SolanaBondedVault` does the same without the kit, with a `dryRun` that simulates `execute` and reads the vault's own events. Needs `@bondedhq/solana`, `@solana/web3.js` and `solana-agent-kit` 2.
454
+
455
+ What the Solana program checks, and so what works:
456
+
457
+ - The called program must be on the vault's allowlist.
458
+ - SPL transfers must go to a token account the vault owns. There is no destination list on Solana, so any transfer out of the vault is a breach.
459
+ - Approvals, `SetAuthority` and closing accounts are never allowed.
460
+ - The net base-asset outflow of one call must stay within the vault's limit. There is no price (T3) check and no 24h limit on Solana yet.
461
+ - One instruction per `execute`, run by CPI with the vault as signer. CPI depth and the compute budget limit which swaps fit; a Jupiter multi-hop route may not. The swap builder (e.g. from Jupiter's swap-instructions API) is yours to supply; the SDK has no DEX client of its own.
462
+
463
+ ## ElizaOS
464
+
465
+ ```ts
466
+ import { bondedPlugin } from "@bondedhq/sdk/eliza";
467
+
468
+ const plugin = bondedPlugin({ vault, tokens, sentinel: { apiUrl, mandate, tokens } });
469
+ // character.plugins / project agent plugins: [plugin]
470
+ ```
471
+
472
+ Actions `BONDED_SWAP`, `BONDED_TRANSFER` and `BONDED_APPROVE` through the vault, and a `BONDED_VAULT` provider that tells the agent the vault's mode, whether it is frozen and its breach count. Parameters come from `options.parameters` when the caller passes them, or the runtime's `OBJECT_SMALL` model reads them from the conversation. Amounts are whole tokens; tokens can be named by symbol. Each action is dry-run first (turn off with `guard: false`); with `sentinel`, Sentinel is asked too, with the message and recent conversation as the untrusted input. Built against `@elizaos/core` 1.7. While a Bonded episode is running the plugin switches to the
473
+ episode's tools: see [Framework helpers](#framework-helpers).
474
+
475
+ ## Development
476
+
477
+ ```bash
478
+ pnpm --filter @bondedhq/sdk test
479
+ ```
480
+
481
+ Tests run against a local anvil with the protocol deployed by the production scripts, and check allowed and breaching actions against the vault's breach log, for the core client and each framework. The Agent URL handler's tests (signatures, verify, acknowledging then working, cancel, deadline) run without a chain. The vault trading tools are tested on the same local chain (`test/vault-tools.int.test.ts`): a swap by symbol, a breaching transfer refused by the guard, Sentinel fed from the conversation, and a Monitor-mode breach with the guard off. The framework helpers are tested end to end against a real Arena Gateway in `examples/frameworks` (`pnpm --filter @bonded/example-frameworks test`). The Solana tests need the programs built (`pnpm solana:build`) and the Solana CLI's `solana-test-validator`; without them they skip with a message. `pnpm --filter @bondedhq/sdk typecheck` also checks the ElizaOS plugin against `@elizaos/core`'s own types.
482
+
483
+ An example agent that uses the SDK is in [`examples/byo-agent`](../../examples/byo-agent/README.md).
@@ -0,0 +1,76 @@
1
+ import { type Address, type Hex, type PublicClient } from "viem";
2
+ /** Swap `amountIn` of `tokenIn` (base units) for `tokenOut`, through the vault's router. */
3
+ export interface SwapParams {
4
+ tokenIn: Address;
5
+ tokenOut: Address;
6
+ amountIn: bigint;
7
+ /** Smallest acceptable output, in `tokenOut` base units. */
8
+ minOut?: bigint;
9
+ }
10
+ /** Send `amount` of `token` (base units) from the vault to `to`. */
11
+ export interface TransferParams {
12
+ token: Address;
13
+ to: Address;
14
+ amount: bigint;
15
+ }
16
+ /** Let `spender` move up to `amount` of the vault's `token`. 0 revokes. */
17
+ export interface ApproveParams {
18
+ token: Address;
19
+ spender: Address;
20
+ amount: bigint;
21
+ }
22
+ /** Any call the vault should make: `target` with calldata `data` (ERC-20 only, no native value). */
23
+ export interface CallParams {
24
+ target: Address;
25
+ data: Hex;
26
+ }
27
+ /** One thing the agent asks its vault to do. */
28
+ export type VaultAction = ({
29
+ type: "swap";
30
+ } & SwapParams) | ({
31
+ type: "transfer";
32
+ } & TransferParams) | ({
33
+ type: "approve";
34
+ } & ApproveParams) | ({
35
+ type: "call";
36
+ } & CallParams);
37
+ /** One `MandateVault.execute(target, data)`. An action becomes one or more of these. */
38
+ export interface VaultCall {
39
+ target: Address;
40
+ data: Hex;
41
+ }
42
+ /**
43
+ * A swap venue the vault's mandate allows. `swap` approves the router for `amountIn` when the
44
+ * vault's allowance is short, then calls it with the vault as the recipient.
45
+ */
46
+ export interface SwapRouter {
47
+ address: Address;
48
+ /** Calldata for the swap; the output must go to `recipient` (the vault). */
49
+ encodeSwap(params: Required<SwapParams> & {
50
+ recipient: Address;
51
+ }): Hex;
52
+ /**
53
+ * Expected output for a swap. When present, a swap whose quote is below `minOut` is refused
54
+ * before anything is sent (for routers whose calldata can't carry a minimum).
55
+ */
56
+ quote?(client: PublicClient, params: SwapParams): Promise<bigint>;
57
+ }
58
+ /**
59
+ * Bonded's testnet market (`DemoRouter`: oracle prices minus a fee). Its `swap` has no minimum
60
+ * output, so `minOut` is checked against its quote before sending.
61
+ */
62
+ export declare function demoRouter(address: Address): SwapRouter;
63
+ /** Checks an action's fields and normalises its addresses, with errors that name the field. */
64
+ export declare function normaliseAction(action: VaultAction): VaultAction;
65
+ /** ERC-20 calldata for a transfer or approval. */
66
+ export declare function erc20Call(fn: "transfer" | "approve", token: Address, who: Address, amount: bigint): VaultCall;
67
+ /** An action in the words Sentinel was trained on: a tool name and string arguments. */
68
+ export declare function describeAction(action: VaultAction, tokens?: readonly {
69
+ symbol: string;
70
+ address: string;
71
+ decimals: number;
72
+ }[]): {
73
+ tool: string;
74
+ args: Record<string, string>;
75
+ };
76
+ //# sourceMappingURL=actions.d.ts.map