@bondedhq/sdk 0.1.5 → 0.1.7
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/README.md +36 -0
- package/dist/agent-url.d.ts +9 -1
- package/dist/agent-url.js +38 -10
- package/dist/episode.d.ts +51 -0
- package/dist/episode.js +40 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/payments.d.ts +1 -1
- package/dist/payments.js +5 -2
- package/dist/x402-remote.d.ts +77 -0
- package/dist/x402-remote.js +88 -0
- package/dist/x402.d.ts +1 -0
- package/dist/x402.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -299,6 +299,21 @@ await x402.close();
|
|
|
299
299
|
- `guardX402Client(client, { guard })` registers a guard and returns the client, if you never close
|
|
300
300
|
budgets yourself. `x402Price(requirements)` gives the payee and exact price it reads.
|
|
301
301
|
|
|
302
|
+
The guard runs in the agent's process, so code that holds the wallet key can still sign without
|
|
303
|
+
it. To stop that, keep the key out of the agent: run [`@bondedhq/signer`](../signer/README.md) on
|
|
304
|
+
your own server, with the key in your KMS, and give the agent `remoteX402Signer` in place of a
|
|
305
|
+
wallet. The signer runs the same budgets and signs only payments that fit:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { remoteX402Signer } from "@bondedhq/sdk/x402";
|
|
309
|
+
|
|
310
|
+
const signer = await remoteX402Signer({ url: "http://bonded-signer:8402", token });
|
|
311
|
+
const client = new x402Client().register("eip155:*", new ExactEvmScheme(signer));
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
A payment the signer won't sign fails with a `BondedError` (`SIGNER_REFUSED`, `SIGNER_UNAVAILABLE`,
|
|
315
|
+
`SIGNER_UNAUTHORIZED` or `INVALID_PAYMENT`); `isPaymentRefusal` recognizes each.
|
|
316
|
+
|
|
302
317
|
## Hosted payment gate (off chain)
|
|
303
318
|
|
|
304
319
|
The same rules as the guard, run by Bonded instead of inside your process. Your vendors, approved
|
|
@@ -373,6 +388,27 @@ if (isBondedEpisode()) {
|
|
|
373
388
|
`isBondedEpisode()` is true when the runner set valid `BONDED_*` variables. `fromBondedEnv` reads
|
|
374
389
|
and checks them, naming every missing or malformed one (`BondedError`, code `EPISODE_ENV`).
|
|
375
390
|
|
|
391
|
+
That is the vault world, where ratings run. An agent that trades through a broker can also be
|
|
392
|
+
tested on the brokerage world (a simulated brokerage account, no vault; docs/BROKERAGE.md), with a
|
|
393
|
+
runner started with `--world brokerage`. Its episode is read with `fromBondedBrokerageEnv()`, and
|
|
394
|
+
`bondedEpisodeWorld()` says which world the runner set up:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { bondedEpisodeWorld, fromBondedBrokerageEnv, fromBondedEnv } from "@bondedhq/sdk";
|
|
398
|
+
|
|
399
|
+
const world = bondedEpisodeWorld(); // "vault", "brokerage", or undefined outside an episode
|
|
400
|
+
if (world === "brokerage") {
|
|
401
|
+
const episode = fromBondedBrokerageEnv(); // { world, episodeId, task, mode: "tools", endpoints, deadlineMs }
|
|
402
|
+
} else if (world === "vault") {
|
|
403
|
+
const episode = fromBondedEnv();
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`isBondedEpisode()` is true in both worlds, so an agent under test never falls through to its
|
|
408
|
+
production tools. In a brokerage episode `fromBondedEnv` and the vault tool helpers throw
|
|
409
|
+
`EPISODE_ENV` rather than guess. By Agent URL, `createBondedHandler` runs brokerage episodes with
|
|
410
|
+
`onBrokerageEpisode` and refuses them (422) without it.
|
|
411
|
+
|
|
376
412
|
The mode decides how the agent acts:
|
|
377
413
|
|
|
378
414
|
- **tools**: reads and writes are tool calls; `swap`, `transfer` and `approve` are tools, and the
|
package/dist/agent-url.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type { BondedEpisode } from "./episode.js";
|
|
2
|
+
import type { BondedBrokerageEpisode, BondedEpisode } from "./episode.js";
|
|
3
3
|
import type { EpisodeUsage } from "./mcp.js";
|
|
4
4
|
/** For your own handler: the proof a verify answer carries, and the whole answer. */
|
|
5
5
|
export { agentUrlVerifyProof, answerAgentUrlVerify } from "@bondedhq/shared/agent-url";
|
|
@@ -40,6 +40,14 @@ export interface BondedHandlerOptions {
|
|
|
40
40
|
secret: string | string[];
|
|
41
41
|
/** Runs one episode. Resolve when the agent is finished; the handler then posts `/done`. */
|
|
42
42
|
onEpisode(episode: BondedEpisode, context: EpisodeContext): Promise<EpisodeOutcome | void> | EpisodeOutcome | void;
|
|
43
|
+
/**
|
|
44
|
+
* Runs one brokerage episode (a simulated brokerage account; docs/BROKERAGE.md). Without it, the
|
|
45
|
+
* handler refuses brokerage episodes with 422, so an agent built for the vault world never gets
|
|
46
|
+
* one by surprise. Errors are passed to `onBrokerageError` (default `console.error`).
|
|
47
|
+
*/
|
|
48
|
+
onBrokerageEpisode?(episode: BondedBrokerageEpisode, context: EpisodeContext): Promise<EpisodeOutcome | void> | EpisodeOutcome | void;
|
|
49
|
+
/** Called when `onBrokerageEpisode` throws (default: `console.error`). `/done` is still posted. */
|
|
50
|
+
onBrokerageError?: (error: unknown, episode: BondedBrokerageEpisode) => void;
|
|
43
51
|
/**
|
|
44
52
|
* Your agent's version (a release tag, a git commit). Reported when Bonded verifies the URL;
|
|
45
53
|
* it becomes the rating's code hash when you don't declare one.
|
package/dist/agent-url.js
CHANGED
|
@@ -32,17 +32,29 @@ export function createBondedHandler(options) {
|
|
|
32
32
|
/** Signatures already accepted, until they fall out of the window: replays are refused. */
|
|
33
33
|
const replay = new AgentUrlReplayGuard(tolerance);
|
|
34
34
|
const running = new Map();
|
|
35
|
-
const
|
|
35
|
+
const onBrokerageError = options.onBrokerageError ??
|
|
36
|
+
((error, episode) => console.error(`Bonded brokerage episode ${episode.episodeId} failed:`, error));
|
|
37
|
+
const run = async (episode, controller, work, reportError) => {
|
|
36
38
|
const deadline = setTimeout(() => controller.abort(new Error("the episode's deadline passed")), episode.deadlineMs);
|
|
39
|
+
// A reporter that throws must not stop `/done` from being posted: the gateway would wait out
|
|
40
|
+
// the deadline for an episode that has already failed.
|
|
41
|
+
const report = (error) => {
|
|
42
|
+
try {
|
|
43
|
+
reportError(error, episode);
|
|
44
|
+
}
|
|
45
|
+
catch (reporterError) {
|
|
46
|
+
console.error(`Bonded episode ${episode.episodeId}: the error reporter threw:`, reporterError);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
37
49
|
let outcome;
|
|
38
50
|
let failed = false;
|
|
39
51
|
try {
|
|
40
|
-
outcome = await
|
|
52
|
+
outcome = await work(episode, { signal: controller.signal });
|
|
41
53
|
}
|
|
42
54
|
catch (error) {
|
|
43
55
|
failed = true;
|
|
44
56
|
if (!controller.signal.aborted)
|
|
45
|
-
|
|
57
|
+
report(error);
|
|
46
58
|
outcome = {
|
|
47
59
|
finalMessage: `the agent failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 4_000),
|
|
48
60
|
};
|
|
@@ -64,7 +76,7 @@ export function createBondedHandler(options) {
|
|
|
64
76
|
}
|
|
65
77
|
catch (error) {
|
|
66
78
|
if (!failed)
|
|
67
|
-
|
|
79
|
+
report(error);
|
|
68
80
|
}
|
|
69
81
|
};
|
|
70
82
|
return async (request, context) => {
|
|
@@ -92,20 +104,25 @@ export function createBondedHandler(options) {
|
|
|
92
104
|
controller?.abort(new Error("Bonded cancelled the episode"));
|
|
93
105
|
return json(200, { ok: true, cancelled: controller !== undefined });
|
|
94
106
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
if (message.world === "brokerage" && !options.onBrokerageEpisode)
|
|
108
|
+
return json(422, {
|
|
109
|
+
error: "this agent doesn't take brokerage episodes (no onBrokerageEpisode)",
|
|
110
|
+
});
|
|
111
|
+
if (running.has(message.episodeId))
|
|
112
|
+
return json(200, { ok: true, episodeId: message.episodeId, alreadyRunning: true });
|
|
98
113
|
if (running.size >= maxConcurrent)
|
|
99
114
|
return json(429, { error: `already running ${running.size} episodes` });
|
|
100
115
|
const controller = new AbortController();
|
|
101
|
-
running.set(
|
|
102
|
-
const work =
|
|
116
|
+
running.set(message.episodeId, controller);
|
|
117
|
+
const work = message.world === "brokerage"
|
|
118
|
+
? run(toBrokerageEpisode(message), controller, (e, c) => options.onBrokerageEpisode(e, c), onBrokerageError)
|
|
119
|
+
: run(toEpisode(message), controller, (e, c) => options.onEpisode(e, c), onError);
|
|
103
120
|
// Called as a method: Cloudflare's ctx.waitUntil needs its `this`.
|
|
104
121
|
if (context?.waitUntil)
|
|
105
122
|
context.waitUntil(work);
|
|
106
123
|
else
|
|
107
124
|
options.waitUntil?.(work);
|
|
108
|
-
return json(202, { ok: true, episodeId:
|
|
125
|
+
return json(202, { ok: true, episodeId: message.episodeId });
|
|
109
126
|
};
|
|
110
127
|
}
|
|
111
128
|
/**
|
|
@@ -122,6 +139,17 @@ async function signingSecret(secrets, body, headers, nowMs, toleranceSeconds) {
|
|
|
122
139
|
}
|
|
123
140
|
return secrets[0];
|
|
124
141
|
}
|
|
142
|
+
/** A brokerage episode as the agent sees it (the same shape `fromBondedBrokerageEnv` returns). */
|
|
143
|
+
function toBrokerageEpisode(message) {
|
|
144
|
+
return {
|
|
145
|
+
world: "brokerage",
|
|
146
|
+
episodeId: message.episodeId,
|
|
147
|
+
task: message.task,
|
|
148
|
+
mode: "tools",
|
|
149
|
+
endpoints: message.endpoints,
|
|
150
|
+
deadlineMs: message.deadlineMs,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
125
153
|
/** The episode as the agent sees it (the same shape `fromBondedEnv` returns). */
|
|
126
154
|
function toEpisode(message) {
|
|
127
155
|
// The runner protocol's message, minus `type`, `nonce` and `teeKeyId` (TEE-bound agents use
|
package/dist/episode.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface BondedEpisode {
|
|
|
41
41
|
* next to `episodeEnv` that writes them, so the runner, the gateway and the SDK can't drift.
|
|
42
42
|
*/
|
|
43
43
|
export declare const bondedEpisodeEnv: import("zod").ZodObject<{
|
|
44
|
+
BONDED_WORLD: import("zod").ZodOptional<import("zod").ZodLiteral<"vault">>;
|
|
44
45
|
BONDED_EPISODE_ID: import("zod").ZodString;
|
|
45
46
|
BONDED_MODE: import("zod").ZodEnum<{
|
|
46
47
|
tools: "tools";
|
|
@@ -57,6 +58,7 @@ export declare const bondedEpisodeEnv: import("zod").ZodObject<{
|
|
|
57
58
|
approvalAllowlist: import("zod").ZodArray<import("zod").ZodPipe<import("zod").ZodString, import("zod").ZodTransform<`0x${string}`, string>>>;
|
|
58
59
|
}, import("zod/v4/core").$strip>>;
|
|
59
60
|
maxSteps: import("zod").ZodNumber;
|
|
61
|
+
tools: import("zod").ZodOptional<import("zod").ZodReadonly<import("zod").ZodArray<import("zod").ZodString>>>;
|
|
60
62
|
}, import("zod/v4/core").$strip>>;
|
|
61
63
|
BONDED_MCP_URL: import("zod").ZodURL;
|
|
62
64
|
BONDED_TOOLS_URL: import("zod").ZodURL;
|
|
@@ -84,6 +86,11 @@ export declare const bondedEpisodeEnv: import("zod").ZodObject<{
|
|
|
84
86
|
* ```ts
|
|
85
87
|
* const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
|
|
86
88
|
* ```
|
|
89
|
+
*
|
|
90
|
+
* True in every world, so an agent under test never falls through to its production tools. In a
|
|
91
|
+
* world the agent wasn't built for, the helpers then fail loudly: in a brokerage episode
|
|
92
|
+
* `fromBondedEnv` and the vault tool helpers throw `EPISODE_ENV`. `bondedEpisodeWorld()` says
|
|
93
|
+
* which world it is.
|
|
87
94
|
*/
|
|
88
95
|
export declare function isBondedEpisode(env?: Record<string, string | undefined>): boolean;
|
|
89
96
|
/**
|
|
@@ -105,4 +112,48 @@ export declare function fromBondedEnv(env?: Record<string, string | undefined>):
|
|
|
105
112
|
* or when the key is held by a TEE (no session key): use your own signer there.
|
|
106
113
|
*/
|
|
107
114
|
export declare function episodeVault(episode: BondedEpisode): BondedVault;
|
|
115
|
+
/**
|
|
116
|
+
* A brokerage episode: the agent manages a simulated brokerage account (stocks, filings, orders,
|
|
117
|
+
* cash withdrawals) through the episode's tools. There is no chain, vault or key: every read and
|
|
118
|
+
* write is a tool call, and the Arena judges it by the brokerage ledger. Needs runner protocol 2
|
|
119
|
+
* (`@bondedhq/runner` 0.1.4 or later), started with `--world brokerage`.
|
|
120
|
+
*/
|
|
121
|
+
export interface BondedBrokerageEpisode {
|
|
122
|
+
world: "brokerage";
|
|
123
|
+
episodeId: string;
|
|
124
|
+
task: AgentTask;
|
|
125
|
+
mode: "tools";
|
|
126
|
+
endpoints: {
|
|
127
|
+
/** MCP over streamable HTTP. */
|
|
128
|
+
mcp: string;
|
|
129
|
+
/** Plain HTTP: `GET {tools}` lists them, `POST {tools}/{name}` with JSON args calls one. */
|
|
130
|
+
tools: string;
|
|
131
|
+
openapi: string;
|
|
132
|
+
/** `POST` here when finished (`{ finalMessage? }`). */
|
|
133
|
+
done: string;
|
|
134
|
+
};
|
|
135
|
+
/** How long the agent has, in milliseconds from when it starts. */
|
|
136
|
+
deadlineMs: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Which world the runner's episode is for: `vault` (on-chain trading from a Mandate Vault, read
|
|
140
|
+
* with `fromBondedEnv`), `brokerage` (read with `fromBondedBrokerageEnv`), or undefined when this
|
|
141
|
+
* process isn't running a Bonded episode.
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* const world = bondedEpisodeWorld();
|
|
145
|
+
* const tools =
|
|
146
|
+
* world === "brokerage"
|
|
147
|
+
* ? await myBrokerageTools(fromBondedBrokerageEnv())
|
|
148
|
+
* : world === "vault"
|
|
149
|
+
* ? await bondedLangChainTools()
|
|
150
|
+
* : productionTools;
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export declare function bondedEpisodeWorld(env?: Record<string, string | undefined>): "vault" | "brokerage" | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* Reads a brokerage episode the Bonded runner set up from the environment. Throws `BondedError`
|
|
156
|
+
* (`EPISODE_ENV`) naming every missing or malformed variable.
|
|
157
|
+
*/
|
|
158
|
+
export declare function fromBondedBrokerageEnv(env?: Record<string, string | undefined>): BondedBrokerageEpisode;
|
|
108
159
|
//# sourceMappingURL=episode.d.ts.map
|
package/dist/episode.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EpisodeEnv, parseEpisodeEnv } from "@bondedhq/shared";
|
|
1
|
+
import { EpisodeEnv, parseBrokerageEpisodeEnv, parseEpisodeEnv, } from "@bondedhq/shared";
|
|
2
2
|
import { defineChain } from "viem";
|
|
3
3
|
import { privateKeyToAccount } from "viem/accounts";
|
|
4
4
|
import { BondedError } from "./errors.js";
|
|
@@ -15,9 +15,14 @@ export const bondedEpisodeEnv = EpisodeEnv;
|
|
|
15
15
|
* ```ts
|
|
16
16
|
* const tools = isBondedEpisode() ? await bondedLangChainTools() : productionTools;
|
|
17
17
|
* ```
|
|
18
|
+
*
|
|
19
|
+
* True in every world, so an agent under test never falls through to its production tools. In a
|
|
20
|
+
* world the agent wasn't built for, the helpers then fail loudly: in a brokerage episode
|
|
21
|
+
* `fromBondedEnv` and the vault tool helpers throw `EPISODE_ENV`. `bondedEpisodeWorld()` says
|
|
22
|
+
* which world it is.
|
|
18
23
|
*/
|
|
19
24
|
export function isBondedEpisode(env = typeof process === "undefined" ? {} : process.env) {
|
|
20
|
-
return
|
|
25
|
+
return bondedEpisodeWorld(env) !== undefined;
|
|
21
26
|
}
|
|
22
27
|
/**
|
|
23
28
|
* Reads the episode the Bonded runner set up from the environment. Throws `BondedError`
|
|
@@ -62,4 +67,37 @@ export function episodeVault(episode) {
|
|
|
62
67
|
router: episode.contracts.exchange,
|
|
63
68
|
});
|
|
64
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Which world the runner's episode is for: `vault` (on-chain trading from a Mandate Vault, read
|
|
72
|
+
* with `fromBondedEnv`), `brokerage` (read with `fromBondedBrokerageEnv`), or undefined when this
|
|
73
|
+
* process isn't running a Bonded episode.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* const world = bondedEpisodeWorld();
|
|
77
|
+
* const tools =
|
|
78
|
+
* world === "brokerage"
|
|
79
|
+
* ? await myBrokerageTools(fromBondedBrokerageEnv())
|
|
80
|
+
* : world === "vault"
|
|
81
|
+
* ? await bondedLangChainTools()
|
|
82
|
+
* : productionTools;
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function bondedEpisodeWorld(env = typeof process === "undefined" ? {} : process.env) {
|
|
86
|
+
// Fails closed: once the runner has named the brokerage world for an episode, that is the
|
|
87
|
+
// answer even if another variable is malformed. `fromBondedBrokerageEnv` then names the bad
|
|
88
|
+
// variable, where "not an episode" would send the agent to its production tools.
|
|
89
|
+
if (env.BONDED_WORLD === "brokerage" && env.BONDED_EPISODE_ID?.trim())
|
|
90
|
+
return "brokerage";
|
|
91
|
+
return parseEpisodeEnv(env).ok ? "vault" : undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reads a brokerage episode the Bonded runner set up from the environment. Throws `BondedError`
|
|
95
|
+
* (`EPISODE_ENV`) naming every missing or malformed variable.
|
|
96
|
+
*/
|
|
97
|
+
export function fromBondedBrokerageEnv(env = process.env) {
|
|
98
|
+
const parsed = parseBrokerageEpisodeEnv(env);
|
|
99
|
+
if (!parsed.ok)
|
|
100
|
+
throw new BondedError("EPISODE_ENV", `not a valid Bonded brokerage episode environment: ${parsed.problems.join("; ")}`);
|
|
101
|
+
return parsed.episode;
|
|
102
|
+
}
|
|
65
103
|
//# sourceMappingURL=episode.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,6 @@ export { BONDED_API_URL, PaymentGate, paymentGate, type GateAnswer, type GateChe
|
|
|
24
24
|
export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, type GuardPaymentsOptions, type GuardVendorChangesOptions, type PaymentBreach, type PaymentGuardOptions, type PaymentGuardState, type PaymentMandate, type PaymentSource, type PaymentVendor, type PaymentVerdict, } from "./payments.js";
|
|
25
25
|
export { describePaymentMandate, GATE_RULE_WORDS, GATE_RULES, PAYMENT_RULE_WORDS, PAYMENT_RULES, type GateRule, type PaidPayment, type PayableInvoice, type PayableRefund, type PaymentAttempt, type PaymentRule, type VerifiedChange, } from "@bondedhq/shared";
|
|
26
26
|
export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, type BondedVaultTool, type BondedVaultToolName, type BondedVaultToolsOptions, } from "./vault-tools.js";
|
|
27
|
-
export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, type BondedEpisode, } from "./episode.js";
|
|
27
|
+
export { bondedEpisodeEnv, bondedEpisodeWorld, episodeVault, fromBondedBrokerageEnv, fromBondedEnv, isBondedEpisode, type BondedBrokerageEpisode, type BondedEpisode, } from "./episode.js";
|
|
28
28
|
export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, type AllowanceCheck, type AllowanceFinding, type TokenRef, installMandateModule, type InstallMandateModuleInput, type ModuleAccountType, type ModuleInstallPlan, } from "./module.js";
|
|
29
29
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,6 @@ export { BONDED_API_URL, PaymentGate, paymentGate, } from "./gate.js";
|
|
|
23
23
|
export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, } from "./payments.js";
|
|
24
24
|
export { describePaymentMandate, GATE_RULE_WORDS, GATE_RULES, PAYMENT_RULE_WORDS, PAYMENT_RULES, } from "@bondedhq/shared";
|
|
25
25
|
export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, } from "./vault-tools.js";
|
|
26
|
-
export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, } from "./episode.js";
|
|
26
|
+
export { bondedEpisodeEnv, bondedEpisodeWorld, episodeVault, fromBondedBrokerageEnv, fromBondedEnv, isBondedEpisode, } from "./episode.js";
|
|
27
27
|
export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, installMandateModule, } from "./module.js";
|
|
28
28
|
//# sourceMappingURL=index.js.map
|
package/dist/payments.d.ts
CHANGED
|
@@ -221,7 +221,7 @@ export interface GuardVendorChangesOptions<I> {
|
|
|
221
221
|
export declare function guardVendorChanges<I, Rest extends unknown[], R>(update: (input: I, ...rest: Rest) => R | Promise<R>, options: GuardVendorChangesOptions<I>): (input: I, ...rest: Rest) => Promise<R>;
|
|
222
222
|
/**
|
|
223
223
|
* The payment guard or gate refused (blocked, frozen, arguments it can't check, or the gate gave no
|
|
224
|
-
* answer to act on): no money moved.
|
|
224
|
+
* answer to act on), or a remote x402 signer didn't sign: no money moved.
|
|
225
225
|
*/
|
|
226
226
|
export declare function isPaymentRefusal(error: unknown): error is PaymentBlockedError | PaymentFrozenError | PaymentGateError | BondedError;
|
|
227
227
|
//# sourceMappingURL=payments.d.ts.map
|
package/dist/payments.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkBudget, checkGeneralPayment, checkInvoicePayment, checkRefundPayment, GATE_RULE_WORDS, isVerifiedChange, normalAccount, utcDay, usdToMicros, } from "@bondedhq/shared";
|
|
2
|
+
import { X402_SIGNER_ERROR_CODES } from "./x402-remote.js";
|
|
2
3
|
import { BudgetBase } from "./budget.js";
|
|
3
4
|
import { BondedError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, } from "./errors.js";
|
|
4
5
|
const read = async (source) => typeof source === "function" ? await source() : (source ?? []);
|
|
@@ -422,13 +423,15 @@ export function guardVendorChanges(update, options) {
|
|
|
422
423
|
}
|
|
423
424
|
/**
|
|
424
425
|
* The payment guard or gate refused (blocked, frozen, arguments it can't check, or the gate gave no
|
|
425
|
-
* answer to act on): no money moved.
|
|
426
|
+
* answer to act on), or a remote x402 signer didn't sign: no money moved.
|
|
426
427
|
*/
|
|
427
428
|
export function isPaymentRefusal(error) {
|
|
428
429
|
return (error instanceof PaymentBlockedError ||
|
|
429
430
|
error instanceof PaymentFrozenError ||
|
|
430
431
|
error instanceof PaymentGateError ||
|
|
431
|
-
(error instanceof BondedError &&
|
|
432
|
+
(error instanceof BondedError &&
|
|
433
|
+
(error.code === "INVALID_PAYMENT" ||
|
|
434
|
+
X402_SIGNER_ERROR_CODES.includes(error.code))));
|
|
432
435
|
}
|
|
433
436
|
const field = (input, name) => {
|
|
434
437
|
const value = input[name];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Address, Hex } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* The wire format between an agent and `@bondedhq/signer`, the x402 signer a business runs on its
|
|
4
|
+
* own server. The agent never holds the wallet key: it sends each x402 payment's EIP-712 request
|
|
5
|
+
* to the signer, which checks it against a budget and signs only if it fits.
|
|
6
|
+
*/
|
|
7
|
+
export declare const X402_SIGNER_PATHS: {
|
|
8
|
+
/** `GET`: the signer's address and the networks it pays on (`X402SignerInfo`). */
|
|
9
|
+
readonly info: "/v1/x402/signer";
|
|
10
|
+
/** `POST` an `X402SignRequest`; answers an `X402SignResponse`, or an `X402SignerProblem`. */
|
|
11
|
+
readonly sign: "/v1/x402/sign";
|
|
12
|
+
};
|
|
13
|
+
/** One EIP-712 request, as the x402 client builds it. Big numbers travel as decimal strings. */
|
|
14
|
+
export interface X402TypedData {
|
|
15
|
+
domain: Record<string, unknown>;
|
|
16
|
+
types: Record<string, unknown>;
|
|
17
|
+
primaryType: string;
|
|
18
|
+
message: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface X402SignRequest {
|
|
21
|
+
typedData: X402TypedData;
|
|
22
|
+
}
|
|
23
|
+
export interface X402SignResponse {
|
|
24
|
+
signature: Hex;
|
|
25
|
+
}
|
|
26
|
+
export interface X402SignerInfo {
|
|
27
|
+
address: Address;
|
|
28
|
+
/** CAIP-2 ids, such as `eip155:8453`. */
|
|
29
|
+
networks: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Why the signer didn't sign. `refused`: the payment breaks the budget or the gate couldn't be
|
|
33
|
+
* reached (it fails closed). `invalid`: not an x402 USDC payment from this signer, or malformed.
|
|
34
|
+
* `unauthorized`: a missing or wrong token. `error`: something broke in the signer.
|
|
35
|
+
*/
|
|
36
|
+
export interface X402SignerProblem {
|
|
37
|
+
error: "refused" | "invalid" | "unauthorized" | "not_found" | "error";
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
/** The error codes `remoteX402Signer` throws; all of them mean nothing was signed. */
|
|
41
|
+
export declare const X402_SIGNER_ERROR_CODES: readonly ["SIGNER_REFUSED", "SIGNER_UNAUTHORIZED", "SIGNER_UNAVAILABLE"];
|
|
42
|
+
export interface RemoteX402SignerOptions {
|
|
43
|
+
/** Where the signer runs, such as `http://bonded-signer:8402`. */
|
|
44
|
+
url: string;
|
|
45
|
+
/** One of the signer's tokens. It can only ask for x402 payments, never move money itself. */
|
|
46
|
+
token: string;
|
|
47
|
+
/**
|
|
48
|
+
* How long to wait for each request, in milliseconds (default 35,000). The signer may need to
|
|
49
|
+
* open a budget with the gate (up to two tries of 10 seconds) and then wait for its KMS (up to
|
|
50
|
+
* 10 seconds); giving up sooner would make the agent retry a payment the signer still counts.
|
|
51
|
+
*/
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
fetch?: typeof fetch;
|
|
54
|
+
}
|
|
55
|
+
/** What an x402 client needs from a signer (`ClientEvmSigner` in `@x402/evm`). */
|
|
56
|
+
export interface RemoteX402Signer {
|
|
57
|
+
readonly address: Address;
|
|
58
|
+
readonly networks: readonly string[];
|
|
59
|
+
signTypedData(typedData: {
|
|
60
|
+
domain: Record<string, unknown>;
|
|
61
|
+
types: Record<string, unknown>;
|
|
62
|
+
primaryType: string;
|
|
63
|
+
message: Record<string, unknown>;
|
|
64
|
+
}): Promise<Hex>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* An x402 signer whose key stays on a server the agent can't reach: `@bondedhq/signer`. Pass it to
|
|
68
|
+
* `ExactEvmScheme` in place of a local account. Each payment goes to the signer, which checks it
|
|
69
|
+
* against the gate's budget for its payee and signs only if it fits, so code in the agent's process
|
|
70
|
+
* can't pay without the check.
|
|
71
|
+
*
|
|
72
|
+
* Nothing is signed when the signer refuses, can't be reached or doesn't accept the token: the
|
|
73
|
+
* x402 client's request fails with a `BondedError` (`SIGNER_REFUSED`, `SIGNER_UNAVAILABLE`,
|
|
74
|
+
* `SIGNER_UNAUTHORIZED`, or `INVALID_PAYMENT`), which `isPaymentRefusal` recognizes.
|
|
75
|
+
*/
|
|
76
|
+
export declare function remoteX402Signer(options: RemoteX402SignerOptions): Promise<RemoteX402Signer>;
|
|
77
|
+
//# sourceMappingURL=x402-remote.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { BondedError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* The wire format between an agent and `@bondedhq/signer`, the x402 signer a business runs on its
|
|
4
|
+
* own server. The agent never holds the wallet key: it sends each x402 payment's EIP-712 request
|
|
5
|
+
* to the signer, which checks it against a budget and signs only if it fits.
|
|
6
|
+
*/
|
|
7
|
+
export const X402_SIGNER_PATHS = {
|
|
8
|
+
/** `GET`: the signer's address and the networks it pays on (`X402SignerInfo`). */
|
|
9
|
+
info: "/v1/x402/signer",
|
|
10
|
+
/** `POST` an `X402SignRequest`; answers an `X402SignResponse`, or an `X402SignerProblem`. */
|
|
11
|
+
sign: "/v1/x402/sign",
|
|
12
|
+
};
|
|
13
|
+
/** The error codes `remoteX402Signer` throws; all of them mean nothing was signed. */
|
|
14
|
+
export const X402_SIGNER_ERROR_CODES = [
|
|
15
|
+
"SIGNER_REFUSED",
|
|
16
|
+
"SIGNER_UNAUTHORIZED",
|
|
17
|
+
"SIGNER_UNAVAILABLE",
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* An x402 signer whose key stays on a server the agent can't reach: `@bondedhq/signer`. Pass it to
|
|
21
|
+
* `ExactEvmScheme` in place of a local account. Each payment goes to the signer, which checks it
|
|
22
|
+
* against the gate's budget for its payee and signs only if it fits, so code in the agent's process
|
|
23
|
+
* can't pay without the check.
|
|
24
|
+
*
|
|
25
|
+
* Nothing is signed when the signer refuses, can't be reached or doesn't accept the token: the
|
|
26
|
+
* x402 client's request fails with a `BondedError` (`SIGNER_REFUSED`, `SIGNER_UNAVAILABLE`,
|
|
27
|
+
* `SIGNER_UNAUTHORIZED`, or `INVALID_PAYMENT`), which `isPaymentRefusal` recognizes.
|
|
28
|
+
*/
|
|
29
|
+
export async function remoteX402Signer(options) {
|
|
30
|
+
const base = options.url.replace(/\/+$/, "");
|
|
31
|
+
const fetchFn = options.fetch ?? fetch;
|
|
32
|
+
const timeoutMs = options.timeoutMs ?? 35_000;
|
|
33
|
+
async function call(path, init = {}) {
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = await fetchFn(`${base}${path}`, {
|
|
37
|
+
...init,
|
|
38
|
+
headers: {
|
|
39
|
+
...(init.body ? { "content-type": "application/json" } : {}),
|
|
40
|
+
authorization: `Bearer ${options.token}`,
|
|
41
|
+
},
|
|
42
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new BondedError("SIGNER_UNAVAILABLE", `the x402 signer can't be reached at ${base}`, {
|
|
47
|
+
cause: error,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const text = await response.text();
|
|
51
|
+
let body;
|
|
52
|
+
try {
|
|
53
|
+
body = JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
body = undefined;
|
|
57
|
+
}
|
|
58
|
+
if (response.ok && body && typeof body === "object")
|
|
59
|
+
return body;
|
|
60
|
+
const problem = body;
|
|
61
|
+
const reason = typeof problem?.reason === "string" ? problem.reason : `HTTP ${response.status}`;
|
|
62
|
+
if (response.status === 401 || problem?.error === "unauthorized")
|
|
63
|
+
throw new BondedError("SIGNER_UNAUTHORIZED", `the x402 signer refused the token: ${reason}`);
|
|
64
|
+
if (problem?.error === "refused")
|
|
65
|
+
throw new BondedError("SIGNER_REFUSED", `the x402 signer refused the payment: ${reason}`);
|
|
66
|
+
if (problem?.error === "invalid")
|
|
67
|
+
throw new BondedError("INVALID_PAYMENT", `the x402 signer won't sign this: ${reason}`);
|
|
68
|
+
throw new BondedError("SIGNER_UNAVAILABLE", `the x402 signer failed: ${reason}`);
|
|
69
|
+
}
|
|
70
|
+
const info = await call(X402_SIGNER_PATHS.info);
|
|
71
|
+
if (typeof info.address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(info.address))
|
|
72
|
+
throw new BondedError("SIGNER_UNAVAILABLE", "the x402 signer sent no valid address");
|
|
73
|
+
return {
|
|
74
|
+
address: info.address,
|
|
75
|
+
networks: Array.isArray(info.networks) ? [...info.networks] : [],
|
|
76
|
+
async signTypedData(typedData) {
|
|
77
|
+
const request = { typedData };
|
|
78
|
+
const { signature } = await call(X402_SIGNER_PATHS.sign, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: JSON.stringify(request, (_key, value) => typeof value === "bigint" ? value.toString() : value),
|
|
81
|
+
});
|
|
82
|
+
if (typeof signature !== "string" || !/^0x[0-9a-fA-F]+$/.test(signature))
|
|
83
|
+
throw new BondedError("SIGNER_UNAVAILABLE", "the x402 signer sent no valid signature");
|
|
84
|
+
return signature;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=x402-remote.js.map
|
package/dist/x402.d.ts
CHANGED
|
@@ -133,4 +133,5 @@ export declare function x402Price(r: X402Requirements, assets?: readonly X402Ass
|
|
|
133
133
|
account: string;
|
|
134
134
|
amountMicros: number;
|
|
135
135
|
};
|
|
136
|
+
export { remoteX402Signer, X402_SIGNER_ERROR_CODES, X402_SIGNER_PATHS, type RemoteX402Signer, type RemoteX402SignerOptions, type X402SignerInfo, type X402SignerProblem, type X402SignRequest, type X402SignResponse, type X402TypedData, } from "./x402-remote.js";
|
|
136
137
|
//# sourceMappingURL=x402.d.ts.map
|
package/dist/x402.js
CHANGED
|
@@ -179,4 +179,5 @@ export function x402Price(r, assets = X402_USD_ASSETS) {
|
|
|
179
179
|
throw new BondedError("INVALID_PAYMENT", "the x402 price is too large");
|
|
180
180
|
return { account: r.payTo, amountMicros: Number(micros) };
|
|
181
181
|
}
|
|
182
|
+
export { remoteX402Signer, X402_SIGNER_ERROR_CODES, X402_SIGNER_PATHS, } from "./x402-remote.js";
|
|
182
183
|
//# sourceMappingURL=x402.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bondedhq/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Trade through a Bonded Mandate Vault and get rated by the Bonded Arena from any TypeScript agent: a typed vault client with dry runs, Sentinel checks and a mandate builder; the Arena episode's tools for LangChain, the Vercel AI SDK, ElizaOS and Coinbase AgentKit; a payment guard for pay tools (LangChain, the AI SDK, MCP, the OpenAI Agents SDK) and x402 clients; and an Agent URL handler.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"@modelcontextprotocol/sdk": "1.30.1",
|
|
95
95
|
"viem": "^2.56.9",
|
|
96
96
|
"zod": "^4.6.5",
|
|
97
|
-
"@bondedhq/shared": "^0.1.
|
|
97
|
+
"@bondedhq/shared": "^0.1.6"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
100
|
"@coinbase/agentkit": "0.10.4",
|