@bondedhq/sdk 0.1.3 → 0.1.5

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/dist/x402.js ADDED
@@ -0,0 +1,182 @@
1
+ import { normalAccount } from "@bondedhq/shared";
2
+ import { BondedError } from "./errors.js";
3
+ import { isPaymentRefusal } from "./payments.js";
4
+ /**
5
+ * USDC on the networks x402 lists it for, by network id (and the version 1 name, where it has one).
6
+ * The guard only counts these (and any `assets` you add) as dollars; a price in anything else is
7
+ * refused.
8
+ */
9
+ export const X402_USD_ASSETS = [
10
+ ...evm(["eip155:8453", "base"], "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"),
11
+ ...evm(["eip155:84532", "base-sepolia"], "0x036CbD53842c5426634e7929541eC2318f3dCF7e"),
12
+ ...evm(["eip155:1"], "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
13
+ ...evm(["eip155:42161"], "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"),
14
+ ...evm(["eip155:137", "polygon"], "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359"),
15
+ ...evm(["eip155:43114", "avalanche"], "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E"),
16
+ ...["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "solana"].map((network) => ({
17
+ network,
18
+ asset: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
19
+ decimals: 6,
20
+ })),
21
+ ...["solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", "solana-devnet"].map((network) => ({
22
+ network,
23
+ asset: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
24
+ decimals: 6,
25
+ })),
26
+ ];
27
+ function evm(networks, asset) {
28
+ return networks.map((network) => ({ network, asset, decimals: 6 }));
29
+ }
30
+ /**
31
+ * Checks every x402 payment against a budget for its service before the x402 client signs it.
32
+ *
33
+ * The first payment to a service opens a budget for it (`guard.budget`): one check against the
34
+ * rules, and with the hosted gate a signed receipt. Every payment then checks against that budget
35
+ * in exact micro-dollars with no network call, so a $0.001 call is fast and counts as $0.001.
36
+ * When a budget is used up or ends, the next payment closes it and opens another.
37
+ *
38
+ * - A refused payment is never signed: the hook aborts and the request fails with the reason. In
39
+ * Monitor mode a payment that breaks the budget is signed and `onBreach` reports it.
40
+ * - A payment the server turns down (it answers 402 again, or settlement fails) is taken back.
41
+ * - Call `close()` when the agent stops, so the unspent part of each budget stops counting toward
42
+ * the day. Unclosed hosted budgets close themselves soon after they end.
43
+ */
44
+ export class X402Guard {
45
+ #options;
46
+ #assets;
47
+ #budgets = new Map();
48
+ #locks = new Map();
49
+ #byRequirements = new WeakMap();
50
+ #byPayload = new WeakMap();
51
+ constructor(options) {
52
+ this.#options = options;
53
+ this.#assets = [...(options.assets ?? []), ...X402_USD_ASSETS];
54
+ }
55
+ /** Registers the guard's hooks on an x402 client and returns the client. */
56
+ register(client) {
57
+ client.onBeforePaymentCreation((context) => this.check(context));
58
+ client.onAfterPaymentCreation?.(async (context) => {
59
+ const counted = this.#byRequirements.get(context.selectedRequirements);
60
+ if (counted)
61
+ this.#byPayload.set(context.paymentPayload, counted);
62
+ });
63
+ client.onPaymentCreationFailure?.(async (context) => {
64
+ const counted = this.#byRequirements.get(context.selectedRequirements);
65
+ if (counted)
66
+ counted.budget.release(counted.spend.id);
67
+ });
68
+ client.onPaymentResponse?.(async (context) => {
69
+ const counted = this.#byPayload.get(context.paymentPayload);
70
+ if (!counted)
71
+ return;
72
+ if (context.settleResponse?.success) {
73
+ if (context.settleResponse.transaction)
74
+ counted.budget.settle(counted.spend.id, context.settleResponse.transaction);
75
+ }
76
+ else if (context.paymentRequired || context.settleResponse?.success === false) {
77
+ counted.budget.release(counted.spend.id);
78
+ }
79
+ });
80
+ return client;
81
+ }
82
+ /** The `onBeforePaymentCreation` hook: counts the payment against its service's budget. */
83
+ async check(context) {
84
+ try {
85
+ const { account, amountMicros } = (this.#options.toPayment ?? ((r) => x402Price(r, this.#assets)))(context.selectedRequirements, context);
86
+ const counted = await this.#exclusive(normalAccount(account), async (key) => {
87
+ const budget = await this.#budgetFor(key, account, amountMicros);
88
+ const spend = await budget.spend(amountMicros, {
89
+ reference: context.paymentRequired.resource?.url?.slice(0, 200) ?? "",
90
+ });
91
+ return { budget, spend };
92
+ });
93
+ this.#byRequirements.set(context.selectedRequirements, counted);
94
+ return undefined;
95
+ }
96
+ catch (error) {
97
+ if (isPaymentRefusal(error))
98
+ return { abort: true, reason: error.message };
99
+ throw error;
100
+ }
101
+ }
102
+ /** The open budgets, one per service. */
103
+ budgets() {
104
+ return [...this.#budgets.values()];
105
+ }
106
+ /** Closes every budget: sends what's left and stops the unspent part counting. */
107
+ async close() {
108
+ const open = [...this.#budgets.values()];
109
+ this.#budgets.clear();
110
+ await Promise.all(open.map((b) => b.close()));
111
+ }
112
+ /** The service's budget, or a new one when there's none, it ended, or the payment won't fit. */
113
+ async #budgetFor(key, account, amountMicros) {
114
+ const current = this.#budgets.get(key);
115
+ if (current && !current.closed) {
116
+ // It fits; or it's over the per-payment limit, which a new budget would have too, so the
117
+ // current one says no.
118
+ if (!current.problem(amountMicros) || amountMicros > current.terms.maxPaymentMicros)
119
+ return current;
120
+ }
121
+ if (current) {
122
+ this.#budgets.delete(key);
123
+ current.close().catch(this.#options.onError ?? (() => undefined));
124
+ }
125
+ const asked = this.#options.budget?.amountUsd ?? 1;
126
+ const budget = await this.#options.guard.budget({
127
+ account,
128
+ amountUsd: Math.max(asked, Math.ceil(amountMicros / 10_000) / 100),
129
+ ...(this.#options.budget?.ttlSeconds === undefined
130
+ ? {}
131
+ : { ttlSeconds: this.#options.budget.ttlSeconds }),
132
+ ...(this.#options.budget?.maxPaymentUsd === undefined
133
+ ? {}
134
+ : { maxPaymentUsd: this.#options.budget.maxPaymentUsd }),
135
+ });
136
+ this.#budgets.set(key, budget);
137
+ return budget;
138
+ }
139
+ /** One payment per service at a time, so two can't both open a budget or both take the last of one. */
140
+ #exclusive(key, run) {
141
+ const next = (this.#locks.get(key) ?? Promise.resolve()).then(() => run(key), () => run(key));
142
+ this.#locks.set(key, next.catch(() => undefined));
143
+ return next;
144
+ }
145
+ }
146
+ /** An x402 guard: see `X402Guard`. */
147
+ export function x402Guard(options) {
148
+ return new X402Guard(options);
149
+ }
150
+ /**
151
+ * Registers a new `X402Guard` on an x402 client and returns the client, for when you never need
152
+ * to close its budgets yourself (hosted budgets close themselves after they end; local ones close
153
+ * when the next budget opens).
154
+ */
155
+ export function guardX402Client(client, options) {
156
+ return new X402Guard(options).register(client);
157
+ }
158
+ const sameAsset = (a, b) => a.startsWith("0x") && b.startsWith("0x") ? a.toLowerCase() === b.toLowerCase() : a === b;
159
+ /**
160
+ * The payee and the exact price in micro-dollars, for a price in a dollar stablecoin the guard
161
+ * knows. Past six decimals it rounds up.
162
+ */
163
+ export function x402Price(r, assets = X402_USD_ASSETS) {
164
+ const units = r.amount ?? r.maxAmountRequired;
165
+ if (typeof units !== "string" || !/^\d+$/.test(units))
166
+ throw new BondedError("INVALID_PAYMENT", "the x402 price must be a whole number of units");
167
+ if (typeof r.payTo !== "string" || !r.payTo)
168
+ throw new BondedError("INVALID_PAYMENT", "the x402 payment has no payTo address");
169
+ const known = assets.find((a) => a.network === r.network && sameAsset(a.asset, r.asset));
170
+ if (!known)
171
+ throw new BondedError("INVALID_PAYMENT", `the x402 price is in ${r.asset} on ${r.network}, not a US dollar stablecoin the guard knows`);
172
+ const value = BigInt(units);
173
+ const micros = known.decimals <= 6
174
+ ? value * 10n ** BigInt(6 - known.decimals)
175
+ : (value + 10n ** BigInt(known.decimals - 6) - 1n) / 10n ** BigInt(known.decimals - 6);
176
+ if (micros === 0n)
177
+ throw new BondedError("INVALID_PAYMENT", "the x402 price is zero");
178
+ if (micros > BigInt(Number.MAX_SAFE_INTEGER))
179
+ throw new BondedError("INVALID_PAYMENT", "the x402 price is too large");
180
+ return { account: r.payTo, amountMicros: Number(micros) };
181
+ }
182
+ //# sourceMappingURL=x402.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bondedhq/sdk",
3
- "version": "0.1.3",
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; and an Agent URL handler.",
3
+ "version": "0.1.5",
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": [
7
7
  "bonded",
@@ -14,6 +14,9 @@
14
14
  "agentkit",
15
15
  "solana-agent-kit",
16
16
  "elizaos",
17
+ "openai-agents",
18
+ "x402",
19
+ "payments",
17
20
  "viem"
18
21
  ],
19
22
  "homepage": "https://github.com/ddboy19912/bonded/tree/main/packages/sdk#readme",
@@ -65,6 +68,14 @@
65
68
  "types": "./dist/eliza.d.ts",
66
69
  "import": "./dist/eliza.js"
67
70
  },
71
+ "./openai-agents": {
72
+ "types": "./dist/openai-agents.d.ts",
73
+ "import": "./dist/openai-agents.js"
74
+ },
75
+ "./x402": {
76
+ "types": "./dist/x402.d.ts",
77
+ "import": "./dist/x402.js"
78
+ },
68
79
  "./package.json": "./package.json"
69
80
  },
70
81
  "files": [
@@ -83,14 +94,17 @@
83
94
  "@modelcontextprotocol/sdk": "1.30.1",
84
95
  "viem": "^2.56.9",
85
96
  "zod": "^4.6.5",
86
- "@bondedhq/shared": "^0.1.3"
97
+ "@bondedhq/shared": "^0.1.5"
87
98
  },
88
99
  "devDependencies": {
89
100
  "@coinbase/agentkit": "0.10.4",
90
101
  "@elizaos/core": "1.7.2",
91
102
  "@langchain/core": "1.2.12",
103
+ "@openai/agents-core": "0.21.0",
92
104
  "@solana/spl-token": "0.4.15",
93
105
  "@solana/web3.js": "^1.98.4",
106
+ "@x402/core": "2.28.0",
107
+ "@x402/fetch": "2.28.0",
94
108
  "ai": "7.0.114",
95
109
  "solana-agent-kit": "2.0.10",
96
110
  "@bondedhq/solana": "0.1.2",