@bondedhq/sdk 0.1.5 → 0.1.6

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 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
@@ -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 && error.code === "INVALID_PAYMENT"));
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.5",
3
+ "version": "0.1.6",
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": [