@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.
- package/LICENSE +21 -0
- package/README.md +482 -2
- package/dist/actions.d.ts +76 -0
- package/dist/actions.js +115 -0
- package/dist/agent-url.d.ts +101 -0
- package/dist/agent-url.js +234 -0
- package/dist/agentkit.d.ts +104 -0
- package/dist/agentkit.js +206 -0
- package/dist/ai-sdk.d.ts +39 -0
- package/dist/ai-sdk.js +86 -0
- package/dist/breaches.d.ts +50 -0
- package/dist/breaches.js +57 -0
- package/dist/done.d.ts +14 -0
- package/dist/done.js +16 -0
- package/dist/eliza.d.ts +118 -0
- package/dist/eliza.js +281 -0
- package/dist/episode.d.ts +108 -0
- package/dist/episode.js +65 -0
- package/dist/errors.d.ts +93 -0
- package/dist/errors.js +97 -0
- package/dist/guarded.d.ts +80 -0
- package/dist/guarded.js +97 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +19 -0
- package/dist/json-schema-zod3.d.ts +9 -0
- package/dist/json-schema-zod3.js +54 -0
- package/dist/langchain.d.ts +39 -0
- package/dist/langchain.js +53 -0
- package/dist/mandate.d.ts +36 -0
- package/dist/mandate.js +26 -0
- package/dist/mcp-config-cli.d.ts +3 -0
- package/dist/mcp-config-cli.js +44 -0
- package/dist/mcp.d.ts +131 -0
- package/dist/mcp.js +111 -0
- package/dist/module.d.ts +37 -0
- package/dist/module.js +32 -0
- package/dist/revert.d.ts +12 -0
- package/dist/revert.js +71 -0
- package/dist/sentinel.d.ts +43 -0
- package/dist/sentinel.js +94 -0
- package/dist/signature.d.ts +34 -0
- package/dist/signature.js +52 -0
- package/dist/solana-agent-kit.d.ts +141 -0
- package/dist/solana-agent-kit.js +333 -0
- package/dist/vault-tools.d.ts +67 -0
- package/dist/vault-tools.js +335 -0
- package/dist/vault.d.ts +116 -0
- package/dist/vault.js +326 -0
- 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
|
-
#
|
|
1
|
+
# @bondedhq/sdk
|
|
2
2
|
|
|
3
|
-
|
|
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
|