@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.
@@ -0,0 +1,474 @@
1
+ import { checkBudget, checkGeneralPayment, checkInvoicePayment, checkRefundPayment, GATE_RULE_WORDS, isVerifiedChange, normalAccount, utcDay, usdToMicros, } from "@bondedhq/shared";
2
+ import { BudgetBase } from "./budget.js";
3
+ import { BondedError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, } from "./errors.js";
4
+ const read = async (source) => typeof source === "function" ? await source() : (source ?? []);
5
+ const copy = (state) => ({
6
+ payments: state.payments.map((p) => ({ ...p })),
7
+ frozen: state.frozen,
8
+ unverifiedChanges: state.unverifiedChanges.map((c) => ({ ...c })),
9
+ budgets: (state.budgets ?? []).map((b) => ({ ...b, terms: { ...b.terms } })),
10
+ budgetSpends: [...(state.budgetSpends ?? [])],
11
+ });
12
+ /** The reference a local budget's reservation is recorded under. */
13
+ const budgetReference = (id) => `budget:${id}`;
14
+ /**
15
+ * The payment mandate, checked inside your own process: the same rules (`@bondedhq/shared`'s
16
+ * payment mandate) the Arena rates agents against, with no network and no model. You give it the
17
+ * mandate and your vendor master, approved invoices and refunds, and verified bank changes; it
18
+ * keeps the payments made and the frozen flag, and hands them to `onStateChange` to persist.
19
+ *
20
+ * Wrap your pay function with `guardPayments` (or a framework helper) so every payment is checked
21
+ * first. In Enforce mode a payment that breaks a rule throws `PaymentBlockedError` and your pay
22
+ * function is never called; in Monitor mode it goes through and `onBreach` reports it. A frozen
23
+ * guard refuses everything (`PaymentFrozenError`).
24
+ *
25
+ * It is a local check: the agent's code can still call the pay function without it, and it makes
26
+ * no receipts. Payments are checked and made one at a time, so two at once can't both pass the
27
+ * duplicate or daily checks.
28
+ */
29
+ export class PaymentGuard {
30
+ mandate;
31
+ #options;
32
+ #state;
33
+ #queue = Promise.resolve();
34
+ #now;
35
+ constructor(options) {
36
+ this.mandate = Object.freeze({ ...options.mandate });
37
+ this.#options = options;
38
+ this.#state = copy(options.state ?? { payments: [], frozen: false, unverifiedChanges: [] });
39
+ this.#now = options.now ?? (() => new Date());
40
+ }
41
+ get frozen() {
42
+ return this.#state.frozen;
43
+ }
44
+ /** A copy of what the guard remembers, to persist or inspect. */
45
+ state() {
46
+ return copy(this.#state);
47
+ }
48
+ /** Lets payments through again after a freeze. For a person who reviewed the breach. */
49
+ unfreeze() {
50
+ return this.#exclusive(async () => {
51
+ if (!this.#state.frozen)
52
+ return;
53
+ this.#state.frozen = false;
54
+ await this.#save();
55
+ });
56
+ }
57
+ /** The rule verdict for a payment, without making or recording it. */
58
+ async check(attempt, kind = "invoice") {
59
+ const check = await this.#check(attempt, kind, utcDay(this.#now()));
60
+ const { rules, amountUsd, reference, vendorId, lossUsd, overpaidUsd } = check;
61
+ return {
62
+ rules,
63
+ reasons: words(rules),
64
+ amountUsd,
65
+ reference,
66
+ vendorId,
67
+ lossUsd,
68
+ overpaidUsd,
69
+ frozen: this.frozen,
70
+ };
71
+ }
72
+ /**
73
+ * Checks a payment, then calls `pay` if the mandate allows it (or Monitor mode lets it through)
74
+ * and records it once `pay` returns. Throws `PaymentBlockedError` (Enforce), `PaymentFrozenError`
75
+ * or `BondedError` `INVALID_PAYMENT` without calling `pay`. A `pay` that throws records nothing.
76
+ */
77
+ pay(attempt, pay, kind = "invoice") {
78
+ return this.#exclusive(async () => {
79
+ if (this.#state.frozen)
80
+ throw new PaymentFrozenError();
81
+ const now = this.#now();
82
+ const day = utcDay(now);
83
+ const check = await this.#check(attempt, kind, day);
84
+ if (check.rules.length > 0) {
85
+ await this.#breach({
86
+ kind,
87
+ rules: check.rules,
88
+ reasons: words(check.rules),
89
+ blocked: this.mandate.mode === "enforce",
90
+ account: attempt.account,
91
+ reference: check.reference,
92
+ vendorId: check.vendorId,
93
+ amountUsd: check.amountUsd,
94
+ lossUsd: check.lossUsd,
95
+ overpaidUsd: check.overpaidUsd,
96
+ day,
97
+ });
98
+ }
99
+ const result = await pay();
100
+ this.#state.payments.push({
101
+ account: attempt.account,
102
+ // A refund settles no supplier invoice, as in the Arena's ledger.
103
+ vendorId: kind === "refund" ? null : check.vendorId,
104
+ amountUsd: check.amountUsd,
105
+ invoiceReference: check.reference,
106
+ day,
107
+ ...(this.mandate.invoiceMatching === false ? { at: now.toISOString() } : {}),
108
+ ...(kind === "refund" ? { kind } : {}),
109
+ });
110
+ await this.#save();
111
+ return result;
112
+ });
113
+ }
114
+ /**
115
+ * Checks a vendor bank change, then calls `update` if a verified ticket covers that vendor and
116
+ * account (or Monitor mode lets it through). An unverified change breaks
117
+ * `unverified_vendor_change`; let through in Monitor mode, its account still never counts as
118
+ * the vendor's.
119
+ */
120
+ changeVendorAccount(request, update) {
121
+ return this.#exclusive(async () => {
122
+ if (this.#state.frozen)
123
+ throw new PaymentFrozenError();
124
+ if (!normalAccount(request.account))
125
+ throw new BondedError("INVALID_PAYMENT", "account is required");
126
+ // Already a known payee for this vendor (its account on file, or a ticket's): no change to
127
+ // verify. Marking it unverified would make the vendor's own account unpayable.
128
+ if (!isVerifiedChange(await this.#payees(), request)) {
129
+ const rules = ["unverified_vendor_change"];
130
+ await this.#breach({
131
+ kind: "vendor_change",
132
+ rules,
133
+ reasons: words(rules),
134
+ blocked: this.mandate.mode === "enforce",
135
+ account: request.account,
136
+ reference: "",
137
+ vendorId: request.vendorId,
138
+ amountUsd: 0,
139
+ lossUsd: 0,
140
+ overpaidUsd: 0,
141
+ day: utcDay(this.#now()),
142
+ });
143
+ this.#state.unverifiedChanges.push({
144
+ vendorId: request.vendorId,
145
+ account: request.account,
146
+ });
147
+ await this.#save();
148
+ }
149
+ return update();
150
+ });
151
+ }
152
+ /**
153
+ * Opens a budget: one check for many small payments to one payee (see `PaymentBudget`). Needs
154
+ * `invoiceMatching: false`. The payee must be a vendor's account (`unknown_payee`), the whole
155
+ * budget must fit in what's left of the day's limit (`over_daily_cap`), and with
156
+ * `maxPaymentsPerHour` there must be room for a payment (`over_hourly_limit`); the budget then
157
+ * covers what's left of the hour. Enforce mode throws `PaymentBlockedError`; Monitor mode opens it
158
+ * and reports the breach. The whole budget counts toward the day until `close()`, when what was
159
+ * spent (rounded up to the cent) counts instead. A budget past its end is closed the next time
160
+ * one is opened.
161
+ */
162
+ budget(request) {
163
+ return this.#exclusive(async () => {
164
+ if (this.#state.frozen)
165
+ throw new PaymentFrozenError();
166
+ if (this.mandate.invoiceMatching !== false)
167
+ throw new BondedError("CONFIG", "budgets need invoiceMatching: false on the mandate");
168
+ const now = this.#now();
169
+ const day = utcDay(now);
170
+ if (this.#settleExpired(now.getTime()))
171
+ await this.#save();
172
+ const ttlSeconds = request.ttlSeconds ?? 3600;
173
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds < 60 || ttlSeconds > 3600)
174
+ throw new BondedError("INVALID_PAYMENT", "ttlSeconds must be a whole number from 60 to 3,600");
175
+ const maxAll = usdToMicros(this.mandate.maxPaymentUsd) ?? 0;
176
+ const asked = request.maxPaymentUsd === undefined ? maxAll : usdToMicros(request.maxPaymentUsd);
177
+ if (asked === undefined || asked <= 0)
178
+ throw new BondedError("INVALID_PAYMENT", "maxPaymentUsd must be positive, with at most six decimals");
179
+ const check = checkBudget({
180
+ limits: {
181
+ ...this.mandate,
182
+ maxPaymentsPerHour: this.mandate.maxPaymentsPerHour ?? null,
183
+ },
184
+ payees: await this.#payees(),
185
+ payments: this.#state.payments,
186
+ paymentsLastHour: this.#paymentsLastHour(),
187
+ reservedPayments: this.#reservedPayments(),
188
+ day,
189
+ }, { account: request.account, budgetUsd: request.amountUsd });
190
+ if (!check.ok)
191
+ throw new BondedError("INVALID_PAYMENT", check.error);
192
+ const id = `local-${crypto.randomUUID()}`;
193
+ if (check.rules.length > 0)
194
+ await this.#breach({
195
+ kind: "budget",
196
+ rules: check.rules,
197
+ reasons: words(check.rules),
198
+ blocked: this.mandate.mode === "enforce",
199
+ account: request.account,
200
+ reference: "",
201
+ vendorId: check.vendorId,
202
+ amountUsd: request.amountUsd,
203
+ lossUsd: 0,
204
+ overpaidUsd: 0,
205
+ day,
206
+ });
207
+ const terms = {
208
+ account: normalAccount(request.account),
209
+ budgetMicros: Math.round(request.amountUsd * 100) * 10_000,
210
+ maxPaymentMicros: Math.min(asked, maxAll),
211
+ maxPayments: check.maxPayments,
212
+ startsAtMs: now.getTime(),
213
+ expiresAtMs: now.getTime() + ttlSeconds * 1000,
214
+ };
215
+ // The reservation: no time, so it doesn't count as a payment for the hourly limit.
216
+ this.#state.payments.push({
217
+ account: request.account,
218
+ vendorId: check.vendorId,
219
+ amountUsd: Math.round(request.amountUsd * 100) / 100,
220
+ invoiceReference: budgetReference(id),
221
+ day,
222
+ });
223
+ const kept = { id, terms, spentMicros: 0, payments: 0 };
224
+ this.#state.budgets = [...(this.#state.budgets ?? []), kept];
225
+ await this.#save();
226
+ return new LocalBudget(id, terms, this.mandate.mode, () => this.#now().getTime(), {
227
+ frozen: () => this.#state.frozen,
228
+ report: (breach) => this.#report(breach),
229
+ recorded: (spend) => this.#exclusive(async () => {
230
+ const b = this.#state.budgets?.find((x) => x.id === id);
231
+ if (b) {
232
+ b.spentMicros += spend.amountMicros;
233
+ b.payments += 1;
234
+ }
235
+ this.#state.budgetSpends = [...(this.#state.budgetSpends ?? []), spend.atMs];
236
+ await this.#save();
237
+ }),
238
+ released: (spend) => void this.#exclusive(async () => {
239
+ const b = this.#state.budgets?.find((x) => x.id === id);
240
+ if (b) {
241
+ b.spentMicros -= spend.amountMicros;
242
+ b.payments -= 1;
243
+ }
244
+ const spends = this.#state.budgetSpends ?? [];
245
+ const at = spends.lastIndexOf(spend.atMs);
246
+ if (at >= 0)
247
+ spends.splice(at, 1);
248
+ await this.#save();
249
+ }),
250
+ close: () => this.#exclusive(async () => {
251
+ if (this.#closeBudget(id))
252
+ await this.#save();
253
+ }),
254
+ });
255
+ });
256
+ }
257
+ /** Payments open budgets may still make, for the hourly limit. A budget past its end makes none. */
258
+ #reservedPayments() {
259
+ const now = this.#now().getTime();
260
+ return (this.#state.budgets ?? []).reduce((sum, b) => b.terms.maxPayments === null || b.terms.expiresAtMs <= now
261
+ ? sum
262
+ : sum + Math.max(0, b.terms.maxPayments - b.payments), 0);
263
+ }
264
+ /** Closes budgets past their end. True when any closed. */
265
+ #settleExpired(nowMs) {
266
+ const expired = (this.#state.budgets ?? []).filter((b) => b.terms.expiresAtMs <= nowMs);
267
+ for (const b of expired)
268
+ this.#closeBudget(b.id);
269
+ return expired.length > 0;
270
+ }
271
+ /** The budget's reservation becomes what it spent, rounded up to the cent. */
272
+ #closeBudget(id) {
273
+ const budget = this.#state.budgets?.find((b) => b.id === id);
274
+ if (!budget)
275
+ return false;
276
+ this.#state.budgets = this.#state.budgets.filter((b) => b.id !== id);
277
+ const at = this.#state.payments.findIndex((p) => p.invoiceReference === budgetReference(id));
278
+ if (at >= 0) {
279
+ const cents = Math.ceil(budget.spentMicros / 10_000);
280
+ if (cents > 0)
281
+ this.#state.payments[at] = { ...this.#state.payments[at], amountUsd: cents / 100 };
282
+ else
283
+ this.#state.payments.splice(at, 1);
284
+ }
285
+ return true;
286
+ }
287
+ /** Runs the shared rule check against the builder's data and the guard's payments. */
288
+ async #check(attempt, kind, day) {
289
+ const payees = await this.#payees();
290
+ const limits = this.mandate;
291
+ const payments = this.#state.payments;
292
+ const check = kind === "refund"
293
+ ? checkRefundPayment({ limits, payees, refunds: await read(this.#options.refunds), payments, day }, attempt)
294
+ : this.mandate.invoiceMatching === false
295
+ ? checkGeneralPayment({
296
+ limits: { ...limits, maxPaymentsPerHour: limits.maxPaymentsPerHour ?? null },
297
+ payees,
298
+ payments,
299
+ // Payments open budgets may still make count toward the hourly limit too.
300
+ paymentsLastHour: this.#paymentsLastHour() + this.#reservedPayments(),
301
+ day,
302
+ }, attempt)
303
+ : checkInvoicePayment({ limits, payees, invoices: await read(this.#options.invoices), payments, day }, attempt);
304
+ if (!check.ok)
305
+ throw new BondedError("INVALID_PAYMENT", check.error);
306
+ return check;
307
+ }
308
+ /**
309
+ * Payments in the last hour, budget payments included. One restored from before the guard kept
310
+ * times doesn't count.
311
+ */
312
+ #paymentsLastHour() {
313
+ const since = this.#now().getTime() - 3_600_000;
314
+ this.#state.budgetSpends = (this.#state.budgetSpends ?? []).filter((at) => at >= since);
315
+ return (this.#state.payments.filter((p) => p.at !== undefined && Date.parse(p.at) >= since).length +
316
+ this.#state.budgetSpends.length);
317
+ }
318
+ /**
319
+ * Known payees: each vendor's account on file, unless it got there through an unverified
320
+ * change, and every account a verified ticket covers.
321
+ */
322
+ async #payees() {
323
+ const unverified = this.#state.unverifiedChanges;
324
+ const onFile = (await read(this.#options.vendors))
325
+ .map((v) => ({ vendorId: v.id, account: v.account }))
326
+ .filter((p) => !unverified.some((u) => u.vendorId === p.vendorId && normalAccount(u.account) === normalAccount(p.account)));
327
+ const ticketed = (await read(this.#options.changes)).map((c) => ({
328
+ vendorId: c.vendorId,
329
+ account: c.account,
330
+ }));
331
+ return [...onFile, ...ticketed];
332
+ }
333
+ /** Reports a breach, freezes if the mandate says so, and throws when Enforce mode blocks it. */
334
+ async #breach(breach) {
335
+ await this.#report(breach);
336
+ if (breach.blocked)
337
+ throw new PaymentBlockedError(breach);
338
+ }
339
+ /** Freezes if the mandate says so, and tells `onBreach`. */
340
+ async #report(breach) {
341
+ if (this.mandate.freezeOnBreach) {
342
+ this.#state.frozen = true;
343
+ await this.#save();
344
+ }
345
+ await this.#options.onBreach?.({
346
+ ...breach,
347
+ rules: [...breach.rules],
348
+ reasons: [...breach.reasons],
349
+ });
350
+ }
351
+ async #save() {
352
+ await this.#options.onStateChange?.(this.state());
353
+ }
354
+ /** One check-and-pay at a time, so the duplicate and daily checks see every payment. */
355
+ #exclusive(run) {
356
+ const next = this.#queue.then(run, run);
357
+ this.#queue = next.catch(() => undefined);
358
+ return next;
359
+ }
360
+ }
361
+ const words = (rules) => rules.map((r) => GATE_RULE_WORDS[r]);
362
+ /** A budget the local guard opened: spends are counted in its state, nothing is sent anywhere. */
363
+ class LocalBudget extends BudgetBase {
364
+ ledger;
365
+ constructor(id, terms, mode, now, ledger) {
366
+ super(id, terms, mode, now, (breach) => ledger.report(breach));
367
+ this.ledger = ledger;
368
+ }
369
+ spend(amountMicros, options) {
370
+ if (this.ledger.frozen())
371
+ return Promise.reject(new PaymentFrozenError());
372
+ return super.spend(amountMicros, options);
373
+ }
374
+ recorded(spend) {
375
+ return this.ledger.recorded(spend);
376
+ }
377
+ releasable() {
378
+ return !this.closed;
379
+ }
380
+ released(spend) {
381
+ this.ledger.released(spend);
382
+ }
383
+ flush() {
384
+ return Promise.resolve();
385
+ }
386
+ async close() {
387
+ this.markClosed();
388
+ await this.ledger.close();
389
+ }
390
+ }
391
+ /** A payment guard: see `PaymentGuard`. */
392
+ export function paymentGuard(options) {
393
+ return new PaymentGuard(options);
394
+ }
395
+ /**
396
+ * Wraps your own pay function so every call is checked against the payment mandate first. The
397
+ * wrapped function takes the same arguments and returns what yours returns. In Enforce mode a
398
+ * payment that breaks a rule throws `PaymentBlockedError` and your function is not called; in
399
+ * Monitor mode it is called and the guard's `onBreach` reports the breach. A frozen guard throws
400
+ * `PaymentFrozenError`.
401
+ *
402
+ * ```ts
403
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
404
+ * const pay = guardPayments(bank.pay, { guard });
405
+ * await pay({ account: "DE89 3704 0044 0532 0130 00", amountUsd: 1840.5, invoiceReference: "ACM-2291" });
406
+ * ```
407
+ *
408
+ * A local check: it makes no receipts, and code that calls your function directly skips it.
409
+ */
410
+ export function guardPayments(pay, options) {
411
+ const toPayment = options.toPayment ?? defaultPayment;
412
+ return async (input, ...rest) => options.guard.pay(toPayment(input), () => pay(input, ...rest), options.kind ?? "invoice");
413
+ }
414
+ /**
415
+ * Wraps your own "change a vendor's bank account" function: a change no verified ticket covers
416
+ * breaks `unverified_vendor_change`. Enforce mode throws `PaymentBlockedError` without calling
417
+ * it; Monitor mode calls it, reports the breach, and never treats the new account as the vendor's.
418
+ */
419
+ export function guardVendorChanges(update, options) {
420
+ const toChange = options.toChange ?? defaultChange;
421
+ return async (input, ...rest) => options.guard.changeVendorAccount(toChange(input), () => update(input, ...rest));
422
+ }
423
+ /**
424
+ * 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
+ */
427
+ export function isPaymentRefusal(error) {
428
+ return (error instanceof PaymentBlockedError ||
429
+ error instanceof PaymentFrozenError ||
430
+ error instanceof PaymentGateError ||
431
+ (error instanceof BondedError && error.code === "INVALID_PAYMENT"));
432
+ }
433
+ const field = (input, name) => {
434
+ const value = input[name];
435
+ if (typeof value !== "string")
436
+ throw new BondedError("INVALID_PAYMENT", `${name} must be a string`);
437
+ return value;
438
+ };
439
+ const asObject = (input) => {
440
+ if (!input || typeof input !== "object")
441
+ throw new BondedError("INVALID_PAYMENT", "the payment's arguments must be an object");
442
+ return input;
443
+ };
444
+ function defaultPayment(input) {
445
+ const args = asObject(input);
446
+ const amount = args.amountUsd;
447
+ const amountUsd = typeof amount === "number"
448
+ ? amount
449
+ : typeof amount === "string" && /^\d+(\.\d+)?$/.test(amount.trim())
450
+ ? Number(amount)
451
+ : NaN;
452
+ if (Number.isNaN(amountUsd))
453
+ throw new BondedError("INVALID_PAYMENT", "amountUsd must be a number of US dollars");
454
+ return {
455
+ account: field(args, "account"),
456
+ amountUsd,
457
+ // Left out, it reaches the rules as empty: only invoice matching needs one.
458
+ ...("invoiceReference" in args || "reference" in args
459
+ ? {
460
+ invoiceReference: field(args, "invoiceReference" in args ? "invoiceReference" : "reference"),
461
+ }
462
+ : {}),
463
+ ...(typeof args.requestId === "string" ? { requestId: args.requestId } : {}),
464
+ };
465
+ }
466
+ function defaultChange(input) {
467
+ const args = asObject(input);
468
+ return {
469
+ vendorId: field(args, "vendorId"),
470
+ account: field(args, "account"),
471
+ ...(typeof args.ticket === "string" ? { ticket: args.ticket } : {}),
472
+ };
473
+ }
474
+ //# sourceMappingURL=payments.js.map
package/dist/x402.d.ts ADDED
@@ -0,0 +1,136 @@
1
+ import type { BudgetRequest, PaymentBudget } from "./budget.js";
2
+ /**
3
+ * One way to pay that an x402 server offers, as `@x402/core` gives it to client hooks. In x402
4
+ * version 2 the price is `amount`; version 1 called it `maxAmountRequired`. Both are in the
5
+ * asset's smallest unit (for USDC, millionths of a dollar).
6
+ */
7
+ export interface X402Requirements {
8
+ scheme: string;
9
+ /** CAIP-2 in version 2 (`eip155:8453`, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`); a name in version 1 (`base`). */
10
+ network: string;
11
+ asset: string;
12
+ payTo: string;
13
+ amount?: string;
14
+ maxAmountRequired?: string;
15
+ resource?: string;
16
+ extra?: Record<string, unknown>;
17
+ }
18
+ /** What `x402Client.onBeforePaymentCreation` hooks are given. */
19
+ export interface X402PaymentContext {
20
+ paymentRequired: {
21
+ x402Version: number;
22
+ resource?: {
23
+ url?: string;
24
+ };
25
+ };
26
+ selectedRequirements: X402Requirements;
27
+ }
28
+ /** A hook result that stops the payment: the x402 client signs nothing. */
29
+ export type X402Abort = {
30
+ abort: true;
31
+ reason: string;
32
+ };
33
+ /** The `x402Client` hooks the guard registers (`@x402/core`'s client, or anything shaped like it). */
34
+ export interface X402ClientHooks {
35
+ onBeforePaymentCreation(hook: (context: X402PaymentContext) => Promise<void | X402Abort>): unknown;
36
+ onAfterPaymentCreation?(hook: (context: X402PaymentContext & {
37
+ paymentPayload: object;
38
+ }) => Promise<void>): unknown;
39
+ onPaymentCreationFailure?(hook: (context: X402PaymentContext & {
40
+ error: Error;
41
+ }) => Promise<void>): unknown;
42
+ onPaymentResponse?(hook: (context: {
43
+ paymentPayload: object;
44
+ settleResponse?: {
45
+ success: boolean;
46
+ transaction?: string;
47
+ };
48
+ paymentRequired?: unknown;
49
+ error?: Error;
50
+ }) => Promise<void>): unknown;
51
+ }
52
+ /** A US dollar stablecoin the guard counts at one dollar per token. */
53
+ export interface X402Asset {
54
+ /** CAIP-2 network id, or an x402 version 1 network name. */
55
+ network: string;
56
+ /** The token's contract or mint address. EVM addresses compare case-insensitively. */
57
+ asset: string;
58
+ decimals: number;
59
+ }
60
+ /**
61
+ * USDC on the networks x402 lists it for, by network id (and the version 1 name, where it has one).
62
+ * The guard only counts these (and any `assets` you add) as dollars; a price in anything else is
63
+ * refused.
64
+ */
65
+ export declare const X402_USD_ASSETS: readonly X402Asset[];
66
+ export interface X402GuardOptions {
67
+ /**
68
+ * A `PaymentGuard`, or the hosted `PaymentGate`: anything with `budget()`. Its mandate needs
69
+ * invoice matching off, and each service's `payTo` address on the vendor list, so the agent
70
+ * pays only services you listed.
71
+ */
72
+ guard: {
73
+ budget(request: BudgetRequest): Promise<PaymentBudget>;
74
+ };
75
+ /**
76
+ * The budget opened for each service: `amountUsd` (default $1, raised to fit a bigger price),
77
+ * `ttlSeconds` (default an hour) and `maxPaymentUsd` (default the mandate's per-payment limit).
78
+ */
79
+ budget?: {
80
+ amountUsd?: number;
81
+ ttlSeconds?: number;
82
+ maxPaymentUsd?: number;
83
+ };
84
+ /** More US dollar stablecoins to accept, beyond `X402_USD_ASSETS`. */
85
+ assets?: readonly X402Asset[];
86
+ /** Reads the payee and the exact price. Default: `payTo`, and the price in micro-dollars. */
87
+ toPayment?: (requirements: X402Requirements, context: X402PaymentContext) => {
88
+ account: string;
89
+ amountMicros: number;
90
+ };
91
+ /** Errors from closing a used-up budget in the background. Default: ignored. */
92
+ onError?: (error: unknown) => void;
93
+ }
94
+ /**
95
+ * Checks every x402 payment against a budget for its service before the x402 client signs it.
96
+ *
97
+ * The first payment to a service opens a budget for it (`guard.budget`): one check against the
98
+ * rules, and with the hosted gate a signed receipt. Every payment then checks against that budget
99
+ * in exact micro-dollars with no network call, so a $0.001 call is fast and counts as $0.001.
100
+ * When a budget is used up or ends, the next payment closes it and opens another.
101
+ *
102
+ * - A refused payment is never signed: the hook aborts and the request fails with the reason. In
103
+ * Monitor mode a payment that breaks the budget is signed and `onBreach` reports it.
104
+ * - A payment the server turns down (it answers 402 again, or settlement fails) is taken back.
105
+ * - Call `close()` when the agent stops, so the unspent part of each budget stops counting toward
106
+ * the day. Unclosed hosted budgets close themselves soon after they end.
107
+ */
108
+ export declare class X402Guard {
109
+ #private;
110
+ constructor(options: X402GuardOptions);
111
+ /** Registers the guard's hooks on an x402 client and returns the client. */
112
+ register<C extends X402ClientHooks>(client: C): C;
113
+ /** The `onBeforePaymentCreation` hook: counts the payment against its service's budget. */
114
+ check(context: X402PaymentContext): Promise<void | X402Abort>;
115
+ /** The open budgets, one per service. */
116
+ budgets(): PaymentBudget[];
117
+ /** Closes every budget: sends what's left and stops the unspent part counting. */
118
+ close(): Promise<void>;
119
+ }
120
+ /** An x402 guard: see `X402Guard`. */
121
+ export declare function x402Guard(options: X402GuardOptions): X402Guard;
122
+ /**
123
+ * Registers a new `X402Guard` on an x402 client and returns the client, for when you never need
124
+ * to close its budgets yourself (hosted budgets close themselves after they end; local ones close
125
+ * when the next budget opens).
126
+ */
127
+ export declare function guardX402Client<C extends X402ClientHooks>(client: C, options: X402GuardOptions): C;
128
+ /**
129
+ * The payee and the exact price in micro-dollars, for a price in a dollar stablecoin the guard
130
+ * knows. Past six decimals it rounds up.
131
+ */
132
+ export declare function x402Price(r: X402Requirements, assets?: readonly X402Asset[]): {
133
+ account: string;
134
+ amountMicros: number;
135
+ };
136
+ //# sourceMappingURL=x402.d.ts.map