@bondedhq/sdk 0.1.2 → 0.1.4

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/gate.js ADDED
@@ -0,0 +1,204 @@
1
+ import { normalAccount, verifyGateReceipt, } from "@bondedhq/shared";
2
+ import { isAddress } from "viem";
3
+ import { BondedError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, } from "./errors.js";
4
+ /** Bonded's hosted API. */
5
+ export const BONDED_API_URL = "https://api-production-ba50.up.railway.app";
6
+ /** One retry of a check the gate may not have answered: its request id makes that safe. */
7
+ const TRIES = 2;
8
+ /**
9
+ * The hosted payment gate: every payment goes to Bonded before it is made, and comes back allowed
10
+ * or blocked with a signed receipt. The rules are the ones the Arena rates agents against, the
11
+ * vendors and invoices are the gate account's (set up at bondedhq.xyz/gate), and the decisions are
12
+ * logged where the owner can see them and anyone can check them.
13
+ *
14
+ * It fails closed: if the gate can't be reached, or its answer doesn't check out, your pay function
15
+ * isn't called and `PaymentGateError` says why. Pass it as `guard` to `guardPayments` or a framework
16
+ * helper in place of a `PaymentGuard`. Like the guard, it only sees payments your code sends it:
17
+ * keep the unwrapped pay function out of the agent's reach.
18
+ */
19
+ export class PaymentGate {
20
+ #options;
21
+ #url;
22
+ #signer;
23
+ constructor(options) {
24
+ if (!options.apiKey)
25
+ throw new BondedError("INVALID_PAYMENT", "apiKey is required");
26
+ this.#options = options;
27
+ this.#url = (options.apiUrl ?? BONDED_API_URL).replace(/\/+$/, "");
28
+ }
29
+ /** Asks the gate about a payment or bank change. An allowed payment counts as made from here. */
30
+ async check(check) {
31
+ // Only what the API takes: it refuses unknown fields, and a caller's `toPayment` may add some.
32
+ const requestId = check.requestId ?? crypto.randomUUID();
33
+ const body = check.kind === "bank_change"
34
+ ? {
35
+ kind: check.kind,
36
+ vendorId: check.vendorId,
37
+ account: check.account,
38
+ ...(check.ticket === undefined ? {} : { ticket: check.ticket }),
39
+ requestId,
40
+ }
41
+ : {
42
+ kind: check.kind ?? "invoice",
43
+ account: check.account,
44
+ amountUsd: check.amountUsd,
45
+ invoiceReference: check.invoiceReference,
46
+ requestId,
47
+ };
48
+ let last;
49
+ for (let attempt = 0; attempt < TRIES; attempt++) {
50
+ try {
51
+ const answer = await this.#request("/payments/check", {
52
+ method: "POST",
53
+ body: JSON.stringify(body),
54
+ });
55
+ await this.#verify(answer, body);
56
+ return answer;
57
+ }
58
+ catch (error) {
59
+ last = error;
60
+ // Only a check the gate may not have answered is worth asking again.
61
+ const unanswered = error instanceof PaymentGateError && (!error.status || error.status >= 500);
62
+ if (!unanswered || error.receipt)
63
+ throw error;
64
+ }
65
+ }
66
+ throw last;
67
+ }
68
+ /**
69
+ * Checks a payment, then calls `pay` if the gate allows it. Throws `PaymentBlockedError`
70
+ * (Enforce), `PaymentFrozenError` or `PaymentGateError` without calling `pay`.
71
+ *
72
+ * The gate counts an allowed payment as made, so if `pay` fails and you try again, pass the same
73
+ * `requestId` both times: the gate gives the first decision back instead of a duplicate breach.
74
+ */
75
+ async pay(attempt, pay, kind = "invoice") {
76
+ await this.#settle(await this.check({ ...attempt, kind }));
77
+ return pay();
78
+ }
79
+ /** The same for a vendor bank change: `update` runs only if the gate allows the change. */
80
+ async changeVendorAccount(request, update) {
81
+ await this.#settle(await this.check({ ...request, kind: "bank_change" }));
82
+ return update();
83
+ }
84
+ /** The gate account's decisions, newest first. Pass `next` back as `before` for older ones. */
85
+ decisions(query = {}) {
86
+ const params = new URLSearchParams();
87
+ for (const [name, value] of Object.entries(query))
88
+ if (value !== undefined)
89
+ params.set(name, String(value));
90
+ return this.#request(`/payments/decisions${params.size ? `?${params}` : ""}`);
91
+ }
92
+ /** One decision with its receipt. */
93
+ decision(id) {
94
+ return this.#request(`/payments/decisions/${encodeURIComponent(id)}`);
95
+ }
96
+ /** A receipt's proof in its day's log root. The gate has one once that UTC day has ended. */
97
+ proof(id) {
98
+ return this.#request(`/payments/decisions/${encodeURIComponent(id)}/proof`);
99
+ }
100
+ /** Reports a breach, then stops the payment unless the gate allowed it. */
101
+ async #settle(answer) {
102
+ if (answer.decision === "frozen")
103
+ throw new PaymentFrozenError();
104
+ if (answer.decision === "breach")
105
+ await this.#options.onBreach?.(breachOf(answer));
106
+ if (!answer.allowed)
107
+ throw new PaymentBlockedError(breachOf(answer));
108
+ }
109
+ async #verify(answer, asked) {
110
+ if (this.#options.verifyReceipts === false)
111
+ return;
112
+ const wrong = (what) => new PaymentGateError(`the gate's answer can't be trusted: ${what}`, undefined, {
113
+ receipt: true,
114
+ });
115
+ const signed = answer.receipt;
116
+ if (!signed?.receipt)
117
+ throw wrong("it has no receipt");
118
+ const signer = await this.#receiptSigner();
119
+ if (!(await verifyGateReceipt(signed, signer).catch(() => false)))
120
+ throw wrong("its receipt isn't signed by Bonded's receipt key");
121
+ const r = signed.receipt;
122
+ const matches = r.decisionId === answer.id &&
123
+ r.decision === answer.decision &&
124
+ r.result === answer.result &&
125
+ answer.allowed === (r.result === "allowed") &&
126
+ r.rules.join() === answer.rules.join() &&
127
+ r.payee === normalAccount(asked.account) &&
128
+ (asked.kind === "bank_change"
129
+ ? r.kind === "bank_change"
130
+ : r.kind === (asked.kind ?? "invoice") &&
131
+ r.amountCents === Math.round(asked.amountUsd * 100) &&
132
+ r.reference === asked.invoiceReference.trim());
133
+ if (!matches)
134
+ throw wrong("its receipt is for a different decision or payment");
135
+ }
136
+ #receiptSigner() {
137
+ if (this.#options.receiptKey)
138
+ return Promise.resolve(this.#options.receiptKey);
139
+ this.#signer ??= this.#request("/gate/receipt-key")
140
+ .then((key) => {
141
+ // Without an address verifyGateReceipt would accept any self-signed receipt.
142
+ if (!isAddress(key?.address))
143
+ throw new PaymentGateError("the gate's receipt key is not an address");
144
+ return key.address;
145
+ })
146
+ .catch((error) => {
147
+ this.#signer = undefined;
148
+ throw error;
149
+ });
150
+ return this.#signer;
151
+ }
152
+ async #request(path, init = {}) {
153
+ const url = `${this.#url}${path}`;
154
+ let response;
155
+ try {
156
+ response = await (this.#options.fetch ?? fetch)(url, {
157
+ ...init,
158
+ headers: {
159
+ Authorization: `Bearer ${this.#options.apiKey}`,
160
+ ...(init.body ? { "Content-Type": "application/json" } : {}),
161
+ },
162
+ signal: AbortSignal.timeout(this.#options.timeoutMs ?? 10_000),
163
+ });
164
+ }
165
+ catch (error) {
166
+ throw new PaymentGateError(`couldn't reach ${url}: ${error.message}`, undefined, {
167
+ cause: error,
168
+ });
169
+ }
170
+ const body = (await response.json().catch(() => undefined));
171
+ if (response.ok && body)
172
+ return body;
173
+ const message = typeof body?.error === "string" ? body.error : `unexpected answer from ${url}`;
174
+ if (response.status === 400)
175
+ throw new BondedError("INVALID_PAYMENT", message);
176
+ throw new PaymentGateError(message, response.status);
177
+ }
178
+ }
179
+ /** The hosted payment gate: see `PaymentGate`. */
180
+ export function paymentGate(options) {
181
+ return new PaymentGate(options);
182
+ }
183
+ function breachOf(answer) {
184
+ return {
185
+ kind: answer.kind === "bank_change"
186
+ ? "vendor_change"
187
+ : answer.kind === "refund"
188
+ ? "refund"
189
+ : "invoice",
190
+ rules: answer.rules,
191
+ reasons: answer.reasons,
192
+ blocked: !answer.allowed,
193
+ account: answer.payee,
194
+ reference: answer.reference,
195
+ vendorId: answer.vendorId,
196
+ amountUsd: answer.amountUsd,
197
+ lossUsd: answer.lossUsd,
198
+ overpaidUsd: answer.overpaidUsd,
199
+ day: answer.createdAt.slice(0, 10),
200
+ decisionId: answer.id,
201
+ receipt: answer.receipt,
202
+ };
203
+ }
204
+ //# sourceMappingURL=gate.js.map
package/dist/index.d.ts CHANGED
@@ -5,6 +5,10 @@
5
5
  * `@bondedhq/sdk/agentkit`, `@bondedhq/sdk/solana-agent-kit`, `@bondedhq/sdk/eliza`,
6
6
  * `@bondedhq/sdk/langchain`, `@bondedhq/sdk/ai-sdk`. The episode's tools over MCP are in
7
7
  * `@bondedhq/sdk/mcp`, and the Agent URL handler in `@bondedhq/sdk/agent-url`.
8
+ *
9
+ * Off chain, `paymentGuard` and `guardPayments` check an agent's payments against the payment
10
+ * mandate inside your own process (no network, no receipts); the framework subpaths wrap a pay
11
+ * tool with it.
8
12
  */
9
13
  export { BondedVault, type ActionResult, type BondedVaultConfig, type VaultStatus, } from "./vault.js";
10
14
  export { demoRouter, describeAction, type ApproveParams, type CallParams, type SwapParams, type SwapRouter, type TransferParams, type VaultAction, type VaultCall, } from "./actions.js";
@@ -12,8 +16,11 @@ export { breachReason, type DryRunResult, type VaultBreach } from "./breaches.js
12
16
  export { guarded, GuardedVault, type GuardCheck, type GuardContext, type GuardedResult, type GuardOptions, type SentinelGuardOptions, type TokenInfo, } from "./guarded.js";
13
17
  export { checkWithSentinel, type MessageSigner, type SentinelCheckRequest, type SentinelInput, } from "./sentinel.js";
14
18
  export { buildMandate, describeMandate, type BuiltMandate, type EvmMandate, type EvmMandateParams, type EvmMandateRules, } from "./mandate.js";
15
- export { BlockedByMandateError, BondedError, CallRevertedError, NotAgentError, PredictedBreachError, SentinelError, SentinelRefusedError, VaultFrozenError, type SentinelVerdict, } from "./errors.js";
19
+ export { BlockedByMandateError, BondedError, CallRevertedError, NotAgentError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, PredictedBreachError, SentinelError, SentinelRefusedError, VaultFrozenError, type SentinelVerdict, } from "./errors.js";
16
20
  export type { BreachType } from "@bondedhq/shared";
21
+ export { BONDED_API_URL, PaymentGate, paymentGate, type GateAnswer, type GateCheck, type GateDecision, type GatePayment, type GateReceiptProof, type GateVendorChange, type PaymentGateOptions, } from "./gate.js";
22
+ 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";
23
+ export { describePaymentMandate, PAYMENT_RULE_WORDS, PAYMENT_RULES, type PaidPayment, type PayableInvoice, type PayableRefund, type PaymentAttempt, type PaymentRule, type VerifiedChange, } from "@bondedhq/shared";
17
24
  export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, type BondedVaultTool, type BondedVaultToolName, type BondedVaultToolsOptions, } from "./vault-tools.js";
18
25
  export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, type BondedEpisode, } from "./episode.js";
19
26
  export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, type AllowanceCheck, type AllowanceFinding, type TokenRef, installMandateModule, type InstallMandateModuleInput, type ModuleAccountType, type ModuleInstallPlan, } from "./module.js";
package/dist/index.js CHANGED
@@ -5,6 +5,10 @@
5
5
  * `@bondedhq/sdk/agentkit`, `@bondedhq/sdk/solana-agent-kit`, `@bondedhq/sdk/eliza`,
6
6
  * `@bondedhq/sdk/langchain`, `@bondedhq/sdk/ai-sdk`. The episode's tools over MCP are in
7
7
  * `@bondedhq/sdk/mcp`, and the Agent URL handler in `@bondedhq/sdk/agent-url`.
8
+ *
9
+ * Off chain, `paymentGuard` and `guardPayments` check an agent's payments against the payment
10
+ * mandate inside your own process (no network, no receipts); the framework subpaths wrap a pay
11
+ * tool with it.
8
12
  */
9
13
  export { BondedVault, } from "./vault.js";
10
14
  export { demoRouter, describeAction, } from "./actions.js";
@@ -12,7 +16,10 @@ export { breachReason } from "./breaches.js";
12
16
  export { guarded, GuardedVault, } from "./guarded.js";
13
17
  export { checkWithSentinel, } from "./sentinel.js";
14
18
  export { buildMandate, describeMandate, } from "./mandate.js";
15
- export { BlockedByMandateError, BondedError, CallRevertedError, NotAgentError, PredictedBreachError, SentinelError, SentinelRefusedError, VaultFrozenError, } from "./errors.js";
19
+ export { BlockedByMandateError, BondedError, CallRevertedError, NotAgentError, PaymentBlockedError, PaymentFrozenError, PaymentGateError, PredictedBreachError, SentinelError, SentinelRefusedError, VaultFrozenError, } from "./errors.js";
20
+ export { BONDED_API_URL, PaymentGate, paymentGate, } from "./gate.js";
21
+ export { guardPayments, guardVendorChanges, isPaymentRefusal, PaymentGuard, paymentGuard, } from "./payments.js";
22
+ export { describePaymentMandate, PAYMENT_RULE_WORDS, PAYMENT_RULES, } from "@bondedhq/shared";
16
23
  export { BONDED_VAULT_TOOLS, bondedVaultFromEnv, bondedVaultTools, } from "./vault-tools.js";
17
24
  export { bondedEpisodeEnv, episodeVault, fromBondedEnv, isBondedEpisode, } from "./episode.js";
18
25
  export { checkUnprotectedAllowances, deploymentTokens, unprotectedAllowancesWarning, installMandateModule, } from "./module.js";
@@ -1,7 +1,9 @@
1
- import { DynamicStructuredTool } from "@langchain/core/tools";
1
+ import { DynamicStructuredTool, type StructuredToolInterface } from "@langchain/core/tools";
2
2
  import { type EpisodeTools, type EpisodeToolsSource } from "./mcp.js";
3
+ import { type GuardPaymentsOptions } from "./payments.js";
3
4
  import { type BondedVaultToolsOptions } from "./vault-tools.js";
4
5
  export type { BondedVaultToolsOptions } from "./vault-tools.js";
6
+ export type { GuardPaymentsOptions } from "./payments.js";
5
7
  /**
6
8
  * The episode's tools as LangChain `StructuredTool`s, for LangChain and LangGraph agents
7
9
  * (`createReactAgent`, `ToolNode`, `model.bindTools`). Each takes the tool's own JSON Schema and
@@ -36,4 +38,20 @@ export declare function bondedLangChainTools(from?: EpisodeTools | EpisodeToolsS
36
38
  * ```
37
39
  */
38
40
  export declare function bondedVaultLangChainTools(options: BondedVaultToolsOptions): DynamicStructuredTool[];
41
+ /**
42
+ * Your own LangChain pay tool, with every call checked against the payment mandate first (see
43
+ * `PaymentGuard`). Same name, description and schema. When the guard refuses (Enforce mode
44
+ * blocked it, the guard is frozen, or it can't read the payment) your tool is not called and the
45
+ * model gets `Error: ...` naming the rules broken, so it can react. In Monitor mode a breaking
46
+ * payment goes through and the guard's `onBreach` reports it. A local check: no receipts.
47
+ *
48
+ * ```ts
49
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
50
+ * const tools = [...readOnlyTools, guardLangChainPayTool(payInvoice, { guard })];
51
+ * ```
52
+ *
53
+ * The payment is read from the tool's arguments: `{ account, amountUsd, invoiceReference }` unless
54
+ * you pass `toPayment`.
55
+ */
56
+ export declare function guardLangChainPayTool(payTool: StructuredToolInterface, options: GuardPaymentsOptions<Record<string, unknown>>): DynamicStructuredTool;
39
57
  //# sourceMappingURL=langchain.d.ts.map
package/dist/langchain.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DynamicStructuredTool } from "@langchain/core/tools";
2
2
  import { resolveTools, resultText } from "./mcp.js";
3
+ import { guardPayments, isPaymentRefusal } from "./payments.js";
3
4
  import { bondedVaultTools } from "./vault-tools.js";
4
5
  /**
5
6
  * The episode's tools as LangChain `StructuredTool`s, for LangChain and LangGraph agents
@@ -50,4 +51,37 @@ export function bondedVaultLangChainTools(options) {
50
51
  func: async (args) => resultText(await t.call(args ?? {})),
51
52
  }));
52
53
  }
54
+ /**
55
+ * Your own LangChain pay tool, with every call checked against the payment mandate first (see
56
+ * `PaymentGuard`). Same name, description and schema. When the guard refuses (Enforce mode
57
+ * blocked it, the guard is frozen, or it can't read the payment) your tool is not called and the
58
+ * model gets `Error: ...` naming the rules broken, so it can react. In Monitor mode a breaking
59
+ * payment goes through and the guard's `onBreach` reports it. A local check: no receipts.
60
+ *
61
+ * ```ts
62
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
63
+ * const tools = [...readOnlyTools, guardLangChainPayTool(payInvoice, { guard })];
64
+ * ```
65
+ *
66
+ * The payment is read from the tool's arguments: `{ account, amountUsd, invoiceReference }` unless
67
+ * you pass `toPayment`.
68
+ */
69
+ export function guardLangChainPayTool(payTool, options) {
70
+ const guarded = guardPayments((args, config) => payTool.invoke(args, config), options);
71
+ return new DynamicStructuredTool({
72
+ name: payTool.name,
73
+ description: payTool.description,
74
+ schema: payTool.schema,
75
+ func: async (args, _runManager, config) => {
76
+ try {
77
+ return await guarded(args ?? {}, config);
78
+ }
79
+ catch (error) {
80
+ if (isPaymentRefusal(error))
81
+ return `Error: ${error.message}`;
82
+ throw error;
83
+ }
84
+ },
85
+ });
86
+ }
53
87
  //# sourceMappingURL=langchain.js.map
package/dist/mcp.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
3
  import type { DoneRequest } from "@bondedhq/shared";
4
+ import { type GuardPaymentsOptions } from "./payments.js";
3
5
  export { isBondedEpisode } from "./episode.js";
4
6
  export { finishEpisode } from "./done.js";
7
+ export type { GuardPaymentsOptions } from "./payments.js";
5
8
  /**
6
9
  * Where an episode's tools are. A `BondedEpisode` (from `fromBondedEnv`, or the one an Agent URL
7
10
  * handler hands you) has these, and so does the runner protocol's `EpisodeMessage`.
@@ -128,4 +131,21 @@ export declare function mcpConfigJson(episode?: {
128
131
  mcp: string;
129
132
  };
130
133
  }, options?: McpConfigOptions): string;
134
+ /**
135
+ * Your MCP server's pay tool handler, with every call checked against the payment mandate first
136
+ * (see `PaymentGuard`). Register the result in its place:
137
+ *
138
+ * ```ts
139
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
140
+ * server.registerTool("pay_invoice", config, guardMcpPayHandler(payInvoice, { guard }));
141
+ * ```
142
+ *
143
+ * When the guard refuses (Enforce mode blocked it, the guard is frozen, or it can't read the
144
+ * payment) your handler is not called and the client gets an error result (`isError: true`)
145
+ * naming the rules broken, so the model can react. In Monitor mode a breaking payment goes
146
+ * through and the guard's `onBreach` reports it. The payment is read from the tool's arguments:
147
+ * `{ account, amountUsd, invoiceReference }` unless you pass `toPayment`. A local check: no
148
+ * receipts.
149
+ */
150
+ export declare function guardMcpPayHandler<I, Rest extends unknown[]>(handler: (args: I, ...rest: Rest) => CallToolResult | Promise<CallToolResult>, options: GuardPaymentsOptions<I>): (args: I, ...rest: Rest) => Promise<CallToolResult>;
131
151
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.js CHANGED
@@ -3,6 +3,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
3
3
  import { BondedError } from "./errors.js";
4
4
  import { finishEpisode } from "./done.js";
5
5
  import { fromBondedEnv } from "./episode.js";
6
+ import { guardPayments, isPaymentRefusal } from "./payments.js";
6
7
  export { isBondedEpisode } from "./episode.js";
7
8
  export { finishEpisode } from "./done.js";
8
9
  /**
@@ -108,4 +109,33 @@ export function mcpConfig(episode = fromBondedEnv(), options = {}) {
108
109
  export function mcpConfigJson(episode = fromBondedEnv(), options = {}) {
109
110
  return `${JSON.stringify(mcpConfig(episode, options), null, 2)}\n`;
110
111
  }
112
+ /**
113
+ * Your MCP server's pay tool handler, with every call checked against the payment mandate first
114
+ * (see `PaymentGuard`). Register the result in its place:
115
+ *
116
+ * ```ts
117
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
118
+ * server.registerTool("pay_invoice", config, guardMcpPayHandler(payInvoice, { guard }));
119
+ * ```
120
+ *
121
+ * When the guard refuses (Enforce mode blocked it, the guard is frozen, or it can't read the
122
+ * payment) your handler is not called and the client gets an error result (`isError: true`)
123
+ * naming the rules broken, so the model can react. In Monitor mode a breaking payment goes
124
+ * through and the guard's `onBreach` reports it. The payment is read from the tool's arguments:
125
+ * `{ account, amountUsd, invoiceReference }` unless you pass `toPayment`. A local check: no
126
+ * receipts.
127
+ */
128
+ export function guardMcpPayHandler(handler, options) {
129
+ const guarded = guardPayments(handler, options);
130
+ return async (args, ...rest) => {
131
+ try {
132
+ return await guarded(args, ...rest);
133
+ }
134
+ catch (error) {
135
+ if (isPaymentRefusal(error))
136
+ return { content: [{ type: "text", text: error.message }], isError: true };
137
+ throw error;
138
+ }
139
+ };
140
+ }
111
141
  //# sourceMappingURL=mcp.js.map
@@ -0,0 +1,194 @@
1
+ import { type PaidPayment, type PayableInvoice, type PayableRefund, type PaymentAttempt, type PaymentLimits, type PaymentRule, type SignedGateReceipt, type VerifiedChange } from "@bondedhq/shared";
2
+ import { BondedError, PaymentBlockedError, PaymentFrozenError, PaymentGateError } from "./errors.js";
3
+ /** The off-chain payment mandate an agent's payments are checked against. */
4
+ export interface PaymentMandate extends PaymentLimits {
5
+ /** Monitor lets a breaking payment through and reports it; Enforce blocks it. */
6
+ mode: "monitor" | "enforce";
7
+ /** After the first breach, refuse every payment until a person unfreezes the guard. */
8
+ freezeOnBreach: boolean;
9
+ }
10
+ /** A supplier as your vendor master holds it. Its account is one the agent may pay. */
11
+ export interface PaymentVendor {
12
+ id: string;
13
+ /** Bank account (an IBAN or account number). Compared without spaces, case-insensitively. */
14
+ account: string;
15
+ name?: string;
16
+ }
17
+ /** A list the guard reads on every check: fixed, or read from your own system each time. */
18
+ export type PaymentSource<T> = readonly T[] | (() => readonly T[] | Promise<readonly T[]>);
19
+ /** What the guard remembers between payments. Persist it to keep it across restarts. */
20
+ export interface PaymentGuardState {
21
+ /** Payments made through the guard (and any made before it, if you restore them), oldest first. */
22
+ payments: PaidPayment[];
23
+ /** Frozen after a breach: every payment is refused until `unfreeze()`. */
24
+ frozen: boolean;
25
+ /**
26
+ * Bank changes made without a verified ticket (let through in Monitor mode). The vendor master
27
+ * may now show these accounts, but they never count as the vendor's: paying one is
28
+ * `unknown_payee`.
29
+ */
30
+ unverifiedChanges: VerifiedChange[];
31
+ }
32
+ /** One payment (or bank change) that broke the mandate, as the guard reports it. */
33
+ export interface PaymentBreach {
34
+ kind: "invoice" | "refund" | "vendor_change";
35
+ /** Every rule broken, most serious first. */
36
+ rules: PaymentRule[];
37
+ /** Each rule in plain words (`PAYMENT_RULE_WORDS`), in the same order. */
38
+ reasons: string[];
39
+ /** Enforce mode stopped it: the pay function was not called. */
40
+ blocked: boolean;
41
+ account: string;
42
+ /** The invoice or refund reference; empty for a bank change. */
43
+ reference: string;
44
+ /** The vendor whose verified account this is (or, for a bank change, the vendor changed). */
45
+ vendorId: string | null;
46
+ amountUsd: number;
47
+ /** If it went out: paid to an account outside the vendor master, gone for good. */
48
+ lossUsd: number;
49
+ /** If it went out: paid to a known payee beyond what it was owed, claimable back. */
50
+ overpaidUsd: number;
51
+ /** UTC day it counts against, YYYY-MM-DD. */
52
+ day: string;
53
+ /** From the hosted gate (`PaymentGate`): the decision's id in its log. */
54
+ decisionId?: string;
55
+ /** From the hosted gate: Bonded's signed receipt for the decision. */
56
+ receipt?: SignedGateReceipt;
57
+ }
58
+ /** The rule verdict for a payment, without making it. */
59
+ export interface PaymentVerdict {
60
+ /** Every rule the payment breaks, most serious first. Empty when it is allowed. */
61
+ rules: PaymentRule[];
62
+ reasons: string[];
63
+ amountUsd: number;
64
+ reference: string;
65
+ vendorId: string | null;
66
+ lossUsd: number;
67
+ overpaidUsd: number;
68
+ /** The guard is frozen: the payment would be refused whatever the rules say. */
69
+ frozen: boolean;
70
+ }
71
+ export interface PaymentGuardOptions {
72
+ mandate: PaymentMandate;
73
+ /** The vendor master: each vendor's account on file. */
74
+ vendors: PaymentSource<PaymentVendor>;
75
+ /** Invoices the business approved for payment. */
76
+ invoices?: PaymentSource<PayableInvoice>;
77
+ /** Customer refunds finance approved. */
78
+ refunds?: PaymentSource<PayableRefund>;
79
+ /**
80
+ * Bank changes finance verified out of band (a call to the number on file). A ticket's account
81
+ * is a known payee for its vendor, and the only way a vendor's account may change.
82
+ */
83
+ changes?: PaymentSource<VerifiedChange>;
84
+ /** State to start from, as `onStateChange` last saved it (or `state()` returned). */
85
+ state?: PaymentGuardState;
86
+ /** Called with the new state after every change, inside the guard's lock. Save it here. */
87
+ onStateChange?: (state: PaymentGuardState) => void | Promise<void>;
88
+ /**
89
+ * Called with every breach before the guard acts on it: blocked in Enforce mode, let through
90
+ * in Monitor mode. If it throws, the payment is not made.
91
+ */
92
+ onBreach?: (breach: PaymentBreach) => void | Promise<void>;
93
+ now?: () => Date;
94
+ }
95
+ /**
96
+ * The payment mandate, checked inside your own process: the same rules (`@bondedhq/shared`'s
97
+ * payment mandate) the Arena rates agents against, with no network and no model. You give it the
98
+ * mandate and your vendor master, approved invoices and refunds, and verified bank changes; it
99
+ * keeps the payments made and the frozen flag, and hands them to `onStateChange` to persist.
100
+ *
101
+ * Wrap your pay function with `guardPayments` (or a framework helper) so every payment is checked
102
+ * first. In Enforce mode a payment that breaks a rule throws `PaymentBlockedError` and your pay
103
+ * function is never called; in Monitor mode it goes through and `onBreach` reports it. A frozen
104
+ * guard refuses everything (`PaymentFrozenError`).
105
+ *
106
+ * It is a local check: the agent's code can still call the pay function without it, and it makes
107
+ * no receipts. Payments are checked and made one at a time, so two at once can't both pass the
108
+ * duplicate or daily checks.
109
+ */
110
+ export declare class PaymentGuard {
111
+ #private;
112
+ readonly mandate: Readonly<PaymentMandate>;
113
+ constructor(options: PaymentGuardOptions);
114
+ get frozen(): boolean;
115
+ /** A copy of what the guard remembers, to persist or inspect. */
116
+ state(): PaymentGuardState;
117
+ /** Lets payments through again after a freeze. For a person who reviewed the breach. */
118
+ unfreeze(): Promise<void>;
119
+ /** The rule verdict for a payment, without making or recording it. */
120
+ check(attempt: PaymentAttempt, kind?: "invoice" | "refund"): Promise<PaymentVerdict>;
121
+ /**
122
+ * Checks a payment, then calls `pay` if the mandate allows it (or Monitor mode lets it through)
123
+ * and records it once `pay` returns. Throws `PaymentBlockedError` (Enforce), `PaymentFrozenError`
124
+ * or `BondedError` `INVALID_PAYMENT` without calling `pay`. A `pay` that throws records nothing.
125
+ */
126
+ pay<R>(attempt: PaymentAttempt, pay: () => R | Promise<R>, kind?: "invoice" | "refund"): Promise<R>;
127
+ /**
128
+ * Checks a vendor bank change, then calls `update` if a verified ticket covers that vendor and
129
+ * account (or Monitor mode lets it through). An unverified change breaks
130
+ * `unverified_vendor_change`; let through in Monitor mode, its account still never counts as
131
+ * the vendor's.
132
+ */
133
+ changeVendorAccount<R>(request: {
134
+ vendorId: string;
135
+ account: string;
136
+ }, update: () => R | Promise<R>): Promise<R>;
137
+ }
138
+ /** A payment guard: see `PaymentGuard`. */
139
+ export declare function paymentGuard(options: PaymentGuardOptions): PaymentGuard;
140
+ export interface GuardPaymentsOptions<I> {
141
+ /**
142
+ * A `PaymentGuard`, or the hosted `PaymentGate`. Share one between your invoice and refund
143
+ * functions, so limits and freezes are shared.
144
+ */
145
+ guard: Pick<PaymentGuard, "pay">;
146
+ /** `invoice` (the default) pays approved invoices; `refund` pays approved customer refunds. */
147
+ kind?: "invoice" | "refund";
148
+ /**
149
+ * Reads the payment from your function's first argument. Default: `{ account, amountUsd,
150
+ * invoiceReference }` (or `reference`), the Arena's `pay_invoice` and `issue_refund` arguments.
151
+ */
152
+ toPayment?: (input: I) => PaymentAttempt;
153
+ }
154
+ /**
155
+ * Wraps your own pay function so every call is checked against the payment mandate first. The
156
+ * wrapped function takes the same arguments and returns what yours returns. In Enforce mode a
157
+ * payment that breaks a rule throws `PaymentBlockedError` and your function is not called; in
158
+ * Monitor mode it is called and the guard's `onBreach` reports the breach. A frozen guard throws
159
+ * `PaymentFrozenError`.
160
+ *
161
+ * ```ts
162
+ * const guard = paymentGuard({ mandate, vendors, invoices, onBreach: alert });
163
+ * const pay = guardPayments(bank.pay, { guard });
164
+ * await pay({ account: "DE89 3704 0044 0532 0130 00", amountUsd: 1840.5, invoiceReference: "ACM-2291" });
165
+ * ```
166
+ *
167
+ * A local check: it makes no receipts, and code that calls your function directly skips it.
168
+ */
169
+ export declare function guardPayments<I, Rest extends unknown[], R>(pay: (input: I, ...rest: Rest) => R | Promise<R>, options: GuardPaymentsOptions<I>): (input: I, ...rest: Rest) => Promise<R>;
170
+ export interface GuardVendorChangesOptions<I> {
171
+ /** A `PaymentGuard`, or the hosted `PaymentGate`. */
172
+ guard: Pick<PaymentGuard, "changeVendorAccount">;
173
+ /**
174
+ * Reads the change from your function's first argument. Default: `{ vendorId, account }`, and
175
+ * `ticket` when there is one (the hosted gate checks it).
176
+ */
177
+ toChange?: (input: I) => {
178
+ vendorId: string;
179
+ account: string;
180
+ ticket?: string;
181
+ };
182
+ }
183
+ /**
184
+ * Wraps your own "change a vendor's bank account" function: a change no verified ticket covers
185
+ * breaks `unverified_vendor_change`. Enforce mode throws `PaymentBlockedError` without calling
186
+ * it; Monitor mode calls it, reports the breach, and never treats the new account as the vendor's.
187
+ */
188
+ 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>;
189
+ /**
190
+ * The payment guard or gate refused (blocked, frozen, arguments it can't check, or the gate gave no
191
+ * answer to act on): no money moved.
192
+ */
193
+ export declare function isPaymentRefusal(error: unknown): error is PaymentBlockedError | PaymentFrozenError | PaymentGateError | BondedError;
194
+ //# sourceMappingURL=payments.d.ts.map