@subly_fi/pay 0.6.2 → 0.7.1

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.
@@ -1,24 +1,367 @@
1
- // src/mcp-server.ts
2
- import { homedir } from "node:os";
3
- import { join as join2 } from "node:path";
1
+ // ../../src/config/vault-catalog.ts
2
+ import { readFileSync } from "node:fs";
3
+ import { z } from "zod";
4
4
 
5
- // ../../src/client/mcp-payment-server.ts
6
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
- import {
9
- CallToolRequestSchema,
10
- ListToolsRequestSchema
11
- } from "@modelcontextprotocol/sdk/types.js";
5
+ // ../../src/lib/solana-address.ts
6
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
7
+ var BASE58_LOOKUP = new Map(
8
+ [...BASE58_ALPHABET].map((character, index) => [character, BigInt(index)])
9
+ );
10
+ function assertSolanaAddress(value, fieldName) {
11
+ if (value.length < 32 || value.length > 44) {
12
+ throw new Error(`${fieldName} must be a valid Solana public key`);
13
+ }
14
+ if (decodeBase58(value).length !== 32) {
15
+ throw new Error(`${fieldName} must be a valid Solana public key`);
16
+ }
17
+ return value;
18
+ }
19
+ function decodeBase58(value) {
20
+ if (value.length === 0) {
21
+ return new Uint8Array();
22
+ }
23
+ let decoded = 0n;
24
+ for (const character of value) {
25
+ const digit = BASE58_LOOKUP.get(character);
26
+ if (digit === void 0) {
27
+ return new Uint8Array();
28
+ }
29
+ decoded = decoded * 58n + digit;
30
+ }
31
+ const bytes = [];
32
+ while (decoded > 0n) {
33
+ bytes.push(Number(decoded & 0xffn));
34
+ decoded >>= 8n;
35
+ }
36
+ for (const character of value) {
37
+ if (character !== "1") {
38
+ break;
39
+ }
40
+ bytes.push(0);
41
+ }
42
+ return Uint8Array.from(bytes.reverse());
43
+ }
12
44
 
13
- // ../../src/api/wallet-auth.ts
45
+ // ../../src/config/vault.ts
46
+ var MAINNET_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
47
+ var KAMINO_VAULT_PROGRAM_ID = "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd";
48
+ var NO_VAULT_FARM = "11111111111111111111111111111111";
49
+ var DEFAULT_VAULT_CONFIG = Object.freeze({
50
+ address: "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr",
51
+ programId: KAMINO_VAULT_PROGRAM_ID,
52
+ usdcMint: MAINNET_USDC_MINT,
53
+ shareMint: "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a",
54
+ farm: "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
55
+ });
56
+ function vaultConfigFromEnv(env = process.env) {
57
+ const value = (name) => env[name]?.trim() || void 0;
58
+ const vaultAddress = value("SUBLY_VAULT_ADDRESS") ?? DEFAULT_VAULT_CONFIG.address;
59
+ const customVault = vaultAddress !== DEFAULT_VAULT_CONFIG.address;
60
+ const anchor = (name, fallback) => {
61
+ const configured = value(name);
62
+ if (customVault && configured === void 0) {
63
+ throw new Error(
64
+ `${name} is required for a custom vault. Generate its settings with npm run configure:vault -- <vault-address>; use ${NO_VAULT_FARM} for a vault without a farm.`
65
+ );
66
+ }
67
+ return assertSolanaAddress(configured ?? fallback, name);
68
+ };
69
+ const usdcMint = value("SUBLY_VAULT_USDC_MINT") ?? MAINNET_USDC_MINT;
70
+ if (usdcMint !== MAINNET_USDC_MINT) {
71
+ throw new Error(
72
+ "SUBLY_VAULT_USDC_MINT must be mainnet USDC; other deposit assets are not supported"
73
+ );
74
+ }
75
+ return Object.freeze({
76
+ address: assertSolanaAddress(vaultAddress, "SUBLY_VAULT_ADDRESS"),
77
+ programId: KAMINO_VAULT_PROGRAM_ID,
78
+ usdcMint,
79
+ shareMint: anchor("SUBLY_VAULT_SHARE_MINT", DEFAULT_VAULT_CONFIG.shareMint),
80
+ farm: anchor("SUBLY_VAULT_FARM", DEFAULT_VAULT_CONFIG.farm)
81
+ });
82
+ }
83
+
84
+ // ../../src/config/vault-catalog.ts
85
+ var publicKey = z.string().refine((value) => {
86
+ try {
87
+ assertSolanaAddress(value, "vault catalog address");
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }, "Invalid Solana public key");
93
+ var catalogSchema = z.object({
94
+ version: z.literal(1),
95
+ defaultVault: publicKey,
96
+ vaults: z.array(z.object({
97
+ address: publicKey,
98
+ programId: z.literal(KAMINO_VAULT_PROGRAM_ID),
99
+ usdcMint: z.literal(MAINNET_USDC_MINT),
100
+ shareMint: publicKey,
101
+ farm: publicKey,
102
+ name: z.string().min(1).max(128).optional(),
103
+ depositsEnabled: z.boolean().optional(),
104
+ extraLookupTables: z.array(publicKey).max(16).optional()
105
+ }).strict()).min(1).max(100)
106
+ }).strict();
107
+ function parseVaultCatalog(value) {
108
+ const catalog2 = catalogSchema.parse(value);
109
+ const addresses = new Set(catalog2.vaults.map((vault) => vault.address));
110
+ if (addresses.size !== catalog2.vaults.length) throw new Error("Duplicate vault in catalog");
111
+ if (!addresses.has(catalog2.defaultVault)) throw new Error("defaultVault must be in the vault catalog");
112
+ return { ...catalog2, vaults: catalog2.vaults.map((vault) => Object.freeze(vault)) };
113
+ }
114
+ function vaultCatalogFromEnv(env = process.env) {
115
+ const path = env.SUBLY_VAULTS_FILE?.trim();
116
+ if (!path) {
117
+ const vault = vaultConfigFromEnv(env);
118
+ return { version: 1, defaultVault: vault.address, vaults: [vault] };
119
+ }
120
+ const catalog2 = parseVaultCatalog(JSON.parse(readFileSync(path, "utf8")));
121
+ const selected2 = env.SUBLY_VAULT_ADDRESS?.trim() || catalog2.defaultVault;
122
+ if (!catalog2.vaults.some((vault) => vault.address === selected2)) {
123
+ throw new Error("SUBLY_VAULT_ADDRESS must be in SUBLY_VAULTS_FILE");
124
+ }
125
+ return { ...catalog2, defaultVault: selected2 };
126
+ }
127
+ function defaultCatalogVault(catalog2) {
128
+ return catalog2.vaults.find((vault) => vault.address === catalog2.defaultVault);
129
+ }
130
+
131
+ // ../../src/client/vault-selection.ts
132
+ var McpVaultSelection = class {
133
+ selectedAddress;
134
+ sessions;
135
+ constructor(sessions2, defaultVault) {
136
+ this.sessions = new Map(sessions2.map((session) => [session.vault.address, session]));
137
+ if (this.sessions.size !== sessions2.length || !this.sessions.has(defaultVault)) {
138
+ throw new Error("MCP vault sessions must be unique and include the default vault");
139
+ }
140
+ this.selectedAddress = defaultVault;
141
+ }
142
+ current() {
143
+ return this.sessions.get(this.selectedAddress);
144
+ }
145
+ list() {
146
+ return {
147
+ selectedVault: this.selectedAddress,
148
+ vaults: [...this.sessions.values()].map(({ vault }) => vault)
149
+ };
150
+ }
151
+ /** The relayer only confirms support; addresses/mints are always pinned locally. */
152
+ async select(address3, relayerBaseUrl2, fetchImpl = fetch) {
153
+ const session = this.sessions.get(address3);
154
+ if (!session) throw new Error("Vault is not in this MCP client's local catalog");
155
+ const response = await fetchImpl(`${relayerBaseUrl2.replace(/\/$/, "")}/v1/vaults`, {
156
+ signal: AbortSignal.timeout(1e4)
157
+ });
158
+ if (!response.ok) throw new Error(`Relayer vault list returned ${response.status}`);
159
+ const body = await response.json();
160
+ const remote = Array.isArray(body.vaults) ? body.vaults.find((vault) => vault?.address === address3) : void 0;
161
+ if (!remote) throw new Error("Vault is not configured on this relayer");
162
+ for (const field of ["programId", "usdcMint", "shareMint", "farm"]) {
163
+ if (remote[field] !== session.vault[field]) {
164
+ throw new Error(`Relayer ${field} differs from the local vault catalog`);
165
+ }
166
+ }
167
+ this.selectedAddress = address3;
168
+ return {
169
+ selectedVault: address3,
170
+ name: session.vault.name ?? null,
171
+ depositsEnabled: session.vault.depositsEnabled !== false && remote.depositsEnabled !== false,
172
+ instructions: "Subsequent tools use this vault. Existing funds stay in their original vault. Each vault needs its own owner setup and spending limits. Selection lasts until changed or this MCP process restarts."
173
+ };
174
+ }
175
+ };
176
+
177
+ // ../../src/config/constants.ts
178
+ var PAYMENT_SCHEME = "subly-yield-exact";
179
+ var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
180
+ var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
181
+ var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
182
+ var SUBLY_VAULT = defaultCatalogVault(vaultCatalogFromEnv());
183
+ var USDC_DECIMALS = 6;
184
+
185
+ // ../../src/lib/associated-token-account.ts
14
186
  import { createHash } from "node:crypto";
15
187
  import bs58 from "bs58";
188
+ var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
189
+ var ED25519_P = (1n << 255n) - 19n;
190
+ var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
191
+ function deriveAssociatedTokenAddress(params) {
192
+ const owner = decodePublicKey(params.owner, "owner");
193
+ const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
194
+ const tokenProgramId = decodePublicKey(
195
+ params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
196
+ "tokenProgramId"
197
+ );
198
+ const associatedTokenProgramId = decodePublicKey(
199
+ ASSOCIATED_TOKEN_PROGRAM_ID,
200
+ "associatedTokenProgramId"
201
+ );
202
+ for (let bump = 255; bump >= 0; bump -= 1) {
203
+ const address3 = createProgramAddress(
204
+ [owner, tokenProgramId, mint, Uint8Array.of(bump)],
205
+ associatedTokenProgramId
206
+ );
207
+ if (address3 !== null) {
208
+ return bs58.encode(address3);
209
+ }
210
+ }
211
+ throw new Error("Unable to derive associated token account address");
212
+ }
213
+ function createProgramAddress(seeds, programId) {
214
+ const hash = createHash("sha256");
215
+ for (const seed of seeds) {
216
+ hash.update(seed);
217
+ }
218
+ hash.update(programId);
219
+ hash.update(PDA_MARKER);
220
+ const digest = hash.digest();
221
+ return isEd25519Point(digest) ? null : new Uint8Array(digest);
222
+ }
223
+ function decodePublicKey(value, fieldName) {
224
+ const decoded = bs58.decode(value);
225
+ if (decoded.length !== 32) {
226
+ throw new Error(`${fieldName} must be a 32-byte public key`);
227
+ }
228
+ return decoded;
229
+ }
230
+ function isEd25519Point(bytes) {
231
+ if (bytes.length !== 32) {
232
+ return false;
233
+ }
234
+ const yBytes = Uint8Array.from(bytes);
235
+ yBytes[31] = yBytes[31] & 127;
236
+ const y = littleEndianToBigInt(yBytes);
237
+ if (y >= ED25519_P) {
238
+ return false;
239
+ }
240
+ const ySquared = mod(y * y, ED25519_P);
241
+ const numerator = mod(ySquared - 1n, ED25519_P);
242
+ const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
243
+ if (denominator === 0n) {
244
+ return false;
245
+ }
246
+ const xSquared = mod(
247
+ numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
248
+ ED25519_P
249
+ );
250
+ return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
251
+ }
252
+ function littleEndianToBigInt(bytes) {
253
+ let value = 0n;
254
+ for (let index = bytes.length - 1; index >= 0; index -= 1) {
255
+ value = (value << 8n) + BigInt(bytes[index]);
256
+ }
257
+ return value;
258
+ }
259
+ function mod(value, modulus) {
260
+ const result = value % modulus;
261
+ return result >= 0n ? result : result + modulus;
262
+ }
263
+ function modPow(base, exponent, modulus) {
264
+ let result = 1n;
265
+ let nextBase = mod(base, modulus);
266
+ let nextExponent = exponent;
267
+ while (nextExponent > 0n) {
268
+ if ((nextExponent & 1n) === 1n) {
269
+ result = mod(result * nextBase, modulus);
270
+ }
271
+ nextBase = mod(nextBase * nextBase, modulus);
272
+ nextExponent >>= 1n;
273
+ }
274
+ return result;
275
+ }
276
+
277
+ // ../../src/client/withdrawal-preview.ts
278
+ var ROUNDING_RAW_USDC = 10n;
279
+ async function assertWithdrawalPreview(input) {
280
+ const destination = deriveAssociatedTokenAddress({ owner: input.wallet, mint: input.vault.usdcMint });
281
+ const simulation = await input.rpc.simulateTransaction(
282
+ input.serializedTransaction,
283
+ {
284
+ encoding: "base64",
285
+ commitment: "confirmed",
286
+ sigVerify: false,
287
+ replaceRecentBlockhash: false,
288
+ innerInstructions: true
289
+ }
290
+ ).send({ abortSignal: AbortSignal.timeout(15e3) });
291
+ if (simulation.value.err !== null) {
292
+ throw new Error("Withdrawal preview failed on the client RPC; no transaction was signed. Check liquidity, RPC and blockhash, then prepare again.");
293
+ }
294
+ let received = 0n;
295
+ for (const group of simulation.value.innerInstructions ?? []) {
296
+ for (const instruction of group.instructions) {
297
+ if (!("parsed" in instruction) || instruction.programId !== SPL_TOKEN_PROGRAM_ID) continue;
298
+ const parsed = instruction.parsed;
299
+ if (parsed.type !== "transfer" && parsed.type !== "transferChecked") continue;
300
+ const info = parsed.info;
301
+ if (!info || info.destination !== destination && info.source !== destination) continue;
302
+ const raw = parsed.type === "transferChecked" ? info.tokenAmount?.amount : info.amount;
303
+ if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
304
+ throw new Error("Withdrawal preview returned an invalid token amount");
305
+ }
306
+ const amount = BigInt(raw);
307
+ if (info.destination === destination) received += amount;
308
+ if (info.source === destination) received -= amount;
309
+ }
310
+ }
311
+ if (received <= 0n || received > input.amountRawUsdc + ROUNDING_RAW_USDC || received < input.amountRawUsdc - ROUNDING_RAW_USDC) {
312
+ throw new Error("Withdrawal preview differs from the requested USDC amount; no transaction was signed");
313
+ }
314
+ }
315
+
316
+ // ../../src/client/lookup-tables.ts
317
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
318
+ import { address, getCompiledTransactionMessageDecoder } from "@solana/kit";
319
+ function lookupTableAddressesForTransaction(serializedTransaction) {
320
+ const wire = Buffer.from(serializedTransaction, "base64");
321
+ let offset = 0;
322
+ let signatureCount = 0;
323
+ let shift = 0;
324
+ while (offset < wire.length) {
325
+ const byte = wire[offset];
326
+ signatureCount |= (byte & 127) << shift;
327
+ offset += 1;
328
+ if ((byte & 128) === 0) {
329
+ break;
330
+ }
331
+ shift += 7;
332
+ }
333
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
334
+ const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
335
+ const lookups = compiled.addressTableLookups ?? [];
336
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
337
+ }
338
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
339
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
340
+ if (addresses.length === 0) {
341
+ return {};
342
+ }
343
+ const tables = await fetchAllMaybeAddressLookupTable(
344
+ rpc2,
345
+ addresses.map((value) => address(value))
346
+ );
347
+ const result = {};
348
+ for (const table of tables) {
349
+ if (table.exists) {
350
+ result[table.address] = table.data.addresses.map(String);
351
+ }
352
+ }
353
+ return result;
354
+ }
355
+
356
+ // ../../src/api/wallet-auth.ts
357
+ import { createHash as createHash2 } from "node:crypto";
358
+ import bs582 from "bs58";
16
359
  import nacl from "tweetnacl";
17
360
  var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
18
361
  var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
19
362
  var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
20
363
  function sha256Hex(data) {
21
- return createHash("sha256").update(data, "utf8").digest("hex");
364
+ return createHash2("sha256").update(data, "utf8").digest("hex");
22
365
  }
23
366
  function walletAuthMessage(params) {
24
367
  return new TextEncoder().encode(
@@ -47,1054 +390,1165 @@ async function walletAuthHeaders(params) {
47
390
  };
48
391
  }
49
392
 
50
- // ../../src/client/onboarding.ts
51
- var SELF_SERVE_POLICY_ID = "self-serve";
52
- var OnboardingError = class extends Error {
53
- constructor(step, message, detail = null) {
393
+ // ../../src/client/vault-flows.ts
394
+ var VaultFlowClientError = class extends Error {
395
+ constructor(step, message, detail = null, code = null, errorDetails = null) {
54
396
  super(message);
55
397
  this.step = step;
56
398
  this.detail = detail;
57
- this.name = "OnboardingError";
399
+ this.code = code;
400
+ this.errorDetails = errorDetails;
401
+ this.name = "VaultFlowClientError";
58
402
  }
59
403
  step;
60
404
  detail;
405
+ code;
406
+ errorDetails;
61
407
  };
62
- async function ensureWalletOnboarded(params) {
63
- const fetchImpl = params.fetchImpl ?? fetch;
64
- const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
65
- const post = async (step, path, body) => {
66
- const url = `${baseUrl}${path}`;
67
- const serialized = JSON.stringify(body);
68
- const response = await fetchImpl(url, {
69
- method: "POST",
70
- headers: {
71
- ...await walletAuthHeaders({
72
- signer: params.signer,
73
- method: "POST",
74
- url,
75
- body: serialized
76
- }),
77
- "content-type": "application/json"
78
- },
79
- body: serialized
80
- });
81
- if (response.status !== 200) {
82
- let detail = null;
83
- try {
84
- detail = await response.json();
85
- } catch {
86
- detail = null;
87
- }
88
- throw new OnboardingError(
89
- step,
90
- `wallet onboarding ${step} failed with ${response.status}`,
91
- detail
92
- );
408
+ var VaultFlowClient = class {
409
+ vault;
410
+ rpc;
411
+ baseUrl;
412
+ signer;
413
+ fetchImpl;
414
+ lookupTablesFor;
415
+ pollTimeoutMs;
416
+ pollIntervalMs;
417
+ constructor(config) {
418
+ this.rpc = config.rpc;
419
+ this.vault = config.vault ?? config.signer.vault ?? SUBLY_VAULT;
420
+ if (config.signer.vault && config.signer.vault.address !== this.vault.address) {
421
+ throw new Error("Vault flow client and signer must select the same vault");
93
422
  }
94
- };
95
- const wallet = params.signer.walletAddress;
96
- await post("register", "/v1/wallets/agent", {
97
- wallet,
98
- signingPolicyId: SELF_SERVE_POLICY_ID,
99
- signingMode: "non_interactive",
100
- signerValidationMode: params.signer.validationMode,
101
- signerProvider: params.signer.provider ?? "local-keypair",
102
- activateForPayments: true
103
- });
104
- await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
105
- }
106
-
107
- // ../../src/x402/headers.ts
108
- import { z } from "zod";
109
-
110
- // ../../src/config/constants.ts
111
- var PAYMENT_SCHEME = "subly-yield-exact";
112
- var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
113
- var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
114
- var ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
115
- var envOr = (name, fallback) => {
116
- const value = process.env[name]?.trim();
117
- return value ? value : fallback;
118
- };
119
- var SUBLY_VAULT = {
120
- name: "Subly USDC Payment Vault Alpha",
121
- address: envOr(
122
- "SUBLY_VAULT_ADDRESS",
123
- "5kfkpQZ6AkQgizHVThqkxD4J3db2i7pE3mHdPNRbx7jr"
124
- ),
125
- programId: "KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd",
126
- usdcMint: envOr(
127
- "SUBLY_VAULT_USDC_MINT",
128
- "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
129
- ),
130
- shareMint: envOr(
131
- "SUBLY_VAULT_SHARE_MINT",
132
- "7hGX49So539MU9Rrah8nBNVYXswWVwEJvgWNYeBDYq3a"
133
- ),
134
- lookupTable: "7UbXhDnpK7WVnwsfivzQRENoqKqAULQ5s19gS1xJrQEo",
135
- farm: envOr(
136
- "SUBLY_VAULT_FARM",
137
- "E2Ct77LowkDAH1T9ubwPpb84pU2GSGrUdgH3KeTTpLX"
138
- )
139
- };
140
- var USDC_DECIMALS = 6;
141
-
142
- // ../../src/lib/hash.ts
143
- import { createHash as createHash2 } from "node:crypto";
144
- var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
145
- function sha256TaggedHex(data) {
146
- return `sha256-${createHash2("sha256").update(data).digest("hex")}`;
147
- }
148
- function stableStringify(value) {
149
- if (value === null) {
150
- return "null";
151
- }
152
- if (typeof value === "bigint") {
153
- return JSON.stringify(value.toString());
154
- }
155
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
156
- return JSON.stringify(value);
157
- }
158
- if (Array.isArray(value)) {
159
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
423
+ this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
424
+ this.signer = config.signer;
425
+ this.fetchImpl = config.fetchImpl ?? fetch;
426
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
427
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
428
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
160
429
  }
161
- const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
162
- return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
163
- }
164
- function hashStableJson(value) {
165
- return sha256TaggedHex(stableStringify(value));
166
- }
167
-
168
- // ../../src/x402/headers.ts
169
- var PAYMENT_REQUIRED_HEADER = "payment-required";
170
- var MAX_HEADER_JSON_BYTES = 16384;
171
- var X402HeaderError = class extends Error {
172
- reason;
173
- constructor(reason, message) {
174
- super(message);
175
- this.name = "X402HeaderError";
176
- this.reason = reason;
430
+ /**
431
+ * Moves USDC from the agent wallet into the vault (fee sponsored). Under
432
+ * depositPolicy "owner_approval_required" the relayer refuses to prepare
433
+ * without an owner approval; when the caller passes none, an already
434
+ * APPROVED deposit approval for this exact amount (e.g. the mandate's
435
+ * initialDeposit — "one Face ID covers mandate + first deposit") is looked
436
+ * up and used automatically before surfacing deposit_approval_required.
437
+ */
438
+ async deposit(input) {
439
+ let approvalId = input.approvalId;
440
+ let prepared;
441
+ try {
442
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
443
+ wallet: this.signer.walletAddress,
444
+ vault: this.vault.address,
445
+ amountRawUsdc: input.amountRawUsdc.toString(),
446
+ ...approvalId === void 0 ? {} : { approvalId }
447
+ });
448
+ } catch (error) {
449
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
450
+ throw error;
451
+ }
452
+ approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
453
+ if (approvalId === void 0) {
454
+ throw error;
455
+ }
456
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
457
+ wallet: this.signer.walletAddress,
458
+ vault: this.vault.address,
459
+ amountRawUsdc: input.amountRawUsdc.toString(),
460
+ approvalId
461
+ });
462
+ }
463
+ if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.signingIntent.amountRawUsdc !== input.amountRawUsdc.toString()) {
464
+ throw new VaultFlowClientError("prepare", "Prepared deposit differs from the requested wallet, vault or amount");
465
+ }
466
+ const signed = await this.signer.signDeposit({
467
+ intent: prepared.signingIntent,
468
+ serializedTransaction: prepared.serializedTransaction,
469
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
470
+ });
471
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
472
+ depositId: prepared.depositId,
473
+ serializedTransaction: signed.serializedTransaction,
474
+ agentSignature: signed.agentSignature
475
+ });
476
+ if (outcome.status === "submitted") {
477
+ outcome = await this.pollUntilTerminal(
478
+ `/v1/deposits/${prepared.depositId}`,
479
+ outcome
480
+ );
481
+ }
482
+ return {
483
+ depositId: prepared.depositId,
484
+ status: outcome.status,
485
+ txSignature: outcome.txSignature ?? null,
486
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
487
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
488
+ errorCode: outcome.errorCode ?? null
489
+ };
177
490
  }
178
- };
179
- var sublyPaymentRequirementsSchema = z.object({
180
- scheme: z.literal(PAYMENT_SCHEME),
181
- network: z.string().min(1),
182
- asset: z.string().min(32),
183
- /** Exact seller amount in raw USDC; the scheme settles exactly this. */
184
- amountRawUsdc: z.string().regex(/^[1-9]\d*$/),
185
- resource: z.string().url(),
186
- description: z.string().optional(),
187
- mimeType: z.string().optional(),
188
- payTo: z.string().min(32),
189
- maxTimeoutSeconds: z.number().int().positive(),
190
- extra: z.object({
191
- sellerRequestId: z.string().min(1),
192
- seller: z.string().min(32),
193
- sellerUsdcAta: z.string().min(32),
194
- vault: z.string().min(32),
195
- shareMint: z.string().min(32)
196
- })
197
- }).loose();
198
- var paymentRequiredSchema = z.object({
199
- x402Version: z.number().int(),
200
- accepts: z.array(z.unknown()),
201
- error: z.string().optional()
202
- }).loose();
203
- var sublyPaymentPayloadSchema = z.object({
204
- x402Version: z.number().int(),
205
- scheme: z.literal(PAYMENT_SCHEME),
206
- network: z.string().min(1),
207
- payload: z.object({
208
- paymentId: z.string().min(1),
209
- requestBindingHash: z.string().min(1),
210
- preparedMessageHash: z.string().min(1),
211
- serializedTransaction: z.string().min(1).max(4096),
212
- agentSignature: z.string().min(1).max(128),
213
- temporarySettlementSignature: z.string().min(1).max(128)
214
- })
215
- }).loose();
216
- function decodeX402Header(headerValue) {
217
- if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
218
- throw new X402HeaderError(
219
- "header_too_large",
220
- `x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
491
+ /**
492
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
493
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
494
+ * with purpose "yield_realize" the relayer refuses anything beyond the
495
+ * spendable yield (the payment path, via RelayerYieldRealizer).
496
+ */
497
+ async withdraw(input) {
498
+ const prepared = await this.postJson(
499
+ "prepare",
500
+ "/v1/withdrawals/prepare",
501
+ {
502
+ wallet: this.signer.walletAddress,
503
+ vault: this.vault.address,
504
+ amountRawUsdc: input.amountRawUsdc.toString(),
505
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
506
+ ...input.payment === void 0 ? {} : { payment: input.payment },
507
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
508
+ }
221
509
  );
510
+ if (prepared.signingIntent?.wallet !== this.signer.walletAddress || prepared.signingIntent.vault !== this.vault.address || prepared.requestedWithdrawRawUsdc !== input.amountRawUsdc.toString() || prepared.purpose !== (input.purpose ?? "normal") || input.purpose === "yield_realize" && prepared.signingIntent.allowFullExit) {
511
+ throw new VaultFlowClientError("prepare", "Prepared withdrawal differs from the requested operation");
512
+ }
513
+ await assertWithdrawalPreview({
514
+ rpc: this.rpc,
515
+ serializedTransaction: prepared.serializedTransaction,
516
+ wallet: this.signer.walletAddress,
517
+ vault: this.vault,
518
+ amountRawUsdc: input.amountRawUsdc
519
+ });
520
+ const signed = await this.signer.signWithdrawal({
521
+ intent: prepared.signingIntent,
522
+ serializedTransaction: prepared.serializedTransaction,
523
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
524
+ });
525
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
526
+ withdrawalId: prepared.withdrawalId,
527
+ serializedTransaction: signed.serializedTransaction,
528
+ agentSignature: signed.agentSignature
529
+ });
530
+ if (outcome.status === "submitted") {
531
+ outcome = await this.pollUntilTerminal(
532
+ `/v1/withdrawals/${prepared.withdrawalId}`,
533
+ outcome
534
+ );
535
+ }
536
+ return {
537
+ withdrawalId: prepared.withdrawalId,
538
+ status: outcome.status,
539
+ txSignature: outcome.txSignature ?? null,
540
+ destinationUsdcAta: prepared.destinationUsdcAta,
541
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
542
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
543
+ errorCode: outcome.errorCode ?? null
544
+ };
222
545
  }
223
- const json = Buffer.from(headerValue, "base64").toString("utf8");
224
- try {
225
- return JSON.parse(json);
226
- } catch {
227
- throw new X402HeaderError(
228
- "invalid_header_encoding",
229
- "x402 header is not base64-encoded JSON"
230
- );
546
+ /**
547
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
548
+ * yield accrued since the last sync shows up); the sync is best-effort and
549
+ * on failure the last-synced view is returned.
550
+ */
551
+ async getBudget(options = {}) {
552
+ if (options.refreshFromChain !== false) {
553
+ try {
554
+ await this.postJson(
555
+ "sync",
556
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
557
+ { source: "chain", vault: this.vault.address }
558
+ );
559
+ } catch {
560
+ }
561
+ }
562
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget?vault=${this.vault.address}`;
563
+ const response = await this.fetchImpl(url, {
564
+ headers: await walletAuthHeaders({
565
+ signer: this.signer,
566
+ method: "GET",
567
+ url
568
+ })
569
+ });
570
+ const text = await response.text();
571
+ if (response.status !== 200) {
572
+ throw new VaultFlowClientError(
573
+ "budget",
574
+ `budget endpoint returned ${response.status}: ${text}`
575
+ );
576
+ }
577
+ let parsed;
578
+ try {
579
+ parsed = JSON.parse(text);
580
+ } catch {
581
+ throw new VaultFlowClientError(
582
+ "budget",
583
+ "budget endpoint returned 200 with a non-JSON body",
584
+ text
585
+ );
586
+ }
587
+ const body = parsed;
588
+ if (body.position?.vault !== void 0 && body.position.vault !== this.vault.address) {
589
+ throw new VaultFlowClientError("budget", "Relayer returned the budget for a different vault");
590
+ }
591
+ return {
592
+ wallet: this.signer.walletAddress,
593
+ vault: this.vault.address,
594
+ principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
595
+ positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
596
+ grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
597
+ spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
598
+ };
231
599
  }
232
- }
233
- function requestBodyHashFor(body) {
234
- if (body === null || body === void 0 || body.length === 0) {
235
- return EMPTY_BODY_HASH;
600
+ /** Best-effort audit link: reports the x402 payment tx a realize funded. */
601
+ async reportPayment(input) {
602
+ await this.postJson("submit", "/v1/payments/report", {
603
+ wallet: this.signer.walletAddress,
604
+ withdrawalId: input.withdrawalId,
605
+ paymentTxSignature: input.paymentTxSignature
606
+ });
236
607
  }
237
- return sha256TaggedHex(
238
- typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
239
- );
240
- }
241
-
242
- // ../../src/client/paid-fetch.ts
243
- function formatRawUsdcAmount(raw) {
244
- const value = BigInt(raw);
245
- const negative = value < 0n;
246
- const abs = negative ? -value : value;
247
- const whole = abs / 1000000n;
248
- const frac = (abs % 1000000n).toString().padStart(6, "0");
249
- return `${negative ? "-" : ""}${whole}.${frac}`;
250
- }
251
-
252
- // ../../src/lib/canonical-json.ts
253
- import { createHash as createHash3 } from "node:crypto";
254
- function sha256HexOf(data) {
255
- return createHash3("sha256").update(data, "utf8").digest("hex");
256
- }
257
-
258
- // ../../src/x402/standard-requirements.ts
259
- import { z as z2 } from "zod";
260
- var STANDARD_EXACT_SCHEME = "exact";
261
- var standardExactRequirementSchema = z2.object({
262
- scheme: z2.literal(STANDARD_EXACT_SCHEME),
263
- /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
264
- network: z2.string().min(1),
265
- /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
266
- asset: z2.string().min(1),
267
- /** Exact price in the asset's atomic units, as a decimal string. */
268
- amount: z2.string().regex(/^[1-9]\d*$/),
269
- /** Recipient wallet; the transfer destination ATA is derived from it. */
270
- payTo: z2.string().min(1),
271
- maxTimeoutSeconds: z2.number().int().positive().optional(),
272
- extra: z2.object({
273
- /** Facilitator address that pays the tx fee (gas sponsorship). */
274
- feePayer: z2.string().min(1).optional()
275
- }).loose().optional()
276
- }).loose();
277
- var standardPaymentRequiredSchema = z2.object({
278
- x402Version: z2.number().int(),
279
- accepts: z2.array(z2.unknown()),
280
- error: z2.string().optional(),
281
- resource: z2.object({ url: z2.string().optional() }).loose().optional()
282
- }).loose();
283
- var StandardX402ChallengeError = class extends Error {
284
- reason;
285
- constructor(reason, message) {
286
- super(message);
287
- this.name = "StandardX402ChallengeError";
288
- this.reason = reason;
608
+ /** Wallet's approvals as the relayer sees them (optionally by status). */
609
+ async listApprovals(status) {
610
+ const body = await this.getJson(
611
+ `/v1/wallets/${this.signer.walletAddress}/approvals${`?vault=${this.vault.address}${status === void 0 ? "" : `&status=${encodeURIComponent(status)}`}`}`
612
+ );
613
+ return body.approvals ?? [];
289
614
  }
290
- };
291
- function parseStandardChallenge(challenge) {
292
- const parsed = standardPaymentRequiredSchema.safeParse(challenge);
293
- if (!parsed.success) {
294
- throw new StandardX402ChallengeError(
295
- "invalid_payment_required",
296
- "Response is not a valid x402 PaymentRequired object"
615
+ /**
616
+ * Creates the owner-onboarding setup link (wallet-auth pins the agreed
617
+ * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
618
+ */
619
+ async createSetupSession(input) {
620
+ const session = await this.postJson(
621
+ "prepare",
622
+ `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
623
+ {
624
+ vault: this.vault.address,
625
+ ...input.policy === void 0 ? {} : { policy: input.policy },
626
+ ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
627
+ ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
628
+ ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
629
+ }
297
630
  );
631
+ if (session.vault !== this.vault.address || session.wallet !== this.signer.walletAddress) {
632
+ throw new VaultFlowClientError("prepare", "Relayer returned a setup session for a different wallet or vault");
633
+ }
634
+ return session;
298
635
  }
299
- const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
300
- const requirement = standardExactRequirementSchema.safeParse(candidate);
301
- if (!requirement.success) {
302
- return [];
636
+ /** Polls a setup session (public capability URL — no auth needed). */
637
+ async getSetupSession(sessionId) {
638
+ const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
639
+ const response = await this.fetchImpl(url);
640
+ const text = await response.text();
641
+ if (response.status !== 200) {
642
+ const parsed = parseRelayerError(text);
643
+ throw new VaultFlowClientError(
644
+ "read",
645
+ parsed.message ?? `setup session read failed with ${response.status}`,
646
+ text,
647
+ parsed.code,
648
+ parsed.details
649
+ );
303
650
  }
304
- return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
305
- });
306
- return { paymentRequired: parsed.data, solanaExactRequirements };
307
- }
308
- function decodeStandardPaymentRequiredHeader(headerValue) {
309
- let decoded;
310
- try {
311
- decoded = decodeX402Header(headerValue);
312
- } catch (error) {
313
- throw new StandardX402ChallengeError(
314
- error instanceof X402HeaderError ? error.reason : "invalid_header",
315
- "Cannot decode the payment-required header"
316
- );
651
+ return JSON.parse(text);
317
652
  }
318
- return parseStandardChallenge(decoded);
319
- }
320
- function selectPayableSolanaRequirement(requirements, options) {
321
- const network = options?.network ?? SOLANA_MAINNET_NETWORK;
322
- const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
323
- const matchingRequirements = requirements.filter(
324
- (candidate) => candidate.network === network && candidate.asset === usdcMint
325
- );
326
- if (matchingRequirements.length === 0) {
327
- throw new StandardX402ChallengeError(
328
- "no_payable_requirement",
329
- `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
330
- );
653
+ /**
654
+ * Finds an APPROVED, unconsumed deposit approval bound to exactly this
655
+ * amount the shape the mandate's initialDeposit approval has.
656
+ */
657
+ async findApprovedDepositApproval(amountRawUsdc) {
658
+ try {
659
+ const approvals = await this.listApprovals("approved");
660
+ const match = approvals.find((approval) => {
661
+ const binding = approval.binding;
662
+ return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
663
+ });
664
+ return match?.approvalId;
665
+ } catch {
666
+ return void 0;
667
+ }
331
668
  }
332
- const requirement = matchingRequirements.find(
333
- (candidate) => candidate.extra?.feePayer !== void 0
334
- ) ?? null;
335
- if (requirement === null) {
336
- throw new StandardX402ChallengeError(
337
- "missing_svm_fee_payer",
338
- "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
339
- );
669
+ /**
670
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
671
+ * (each read looks the tx up on-chain) or the timeout elapses.
672
+ */
673
+ async pollUntilTerminal(path, last) {
674
+ const deadline = Date.now() + this.pollTimeoutMs;
675
+ let latest = last;
676
+ while (Date.now() < deadline) {
677
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
678
+ const url = `${this.baseUrl}${path}`;
679
+ const response = await this.fetchImpl(url, {
680
+ headers: await walletAuthHeaders({
681
+ signer: this.signer,
682
+ method: "GET",
683
+ url
684
+ })
685
+ });
686
+ if (response.status !== 200) {
687
+ continue;
688
+ }
689
+ try {
690
+ latest = await response.json();
691
+ } catch {
692
+ continue;
693
+ }
694
+ if (latest.status !== "submitted") {
695
+ return latest;
696
+ }
697
+ }
698
+ return latest;
340
699
  }
341
- const feePayer = requirement.extra?.feePayer;
342
- if (feePayer === void 0) {
343
- throw new StandardX402ChallengeError(
344
- "missing_svm_fee_payer",
345
- "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
346
- );
700
+ async postJson(step, path, body) {
701
+ const url = `${this.baseUrl}${path}`;
702
+ const serialized = JSON.stringify(body);
703
+ const response = await this.fetchImpl(url, {
704
+ method: "POST",
705
+ headers: {
706
+ ...await walletAuthHeaders({
707
+ signer: this.signer,
708
+ method: "POST",
709
+ url,
710
+ body: serialized
711
+ }),
712
+ "content-type": "application/json"
713
+ },
714
+ body: serialized
715
+ });
716
+ const text = await response.text();
717
+ if (response.status !== 200) {
718
+ const parsed = parseRelayerError(text);
719
+ throw new VaultFlowClientError(
720
+ step,
721
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
722
+ text,
723
+ parsed.code,
724
+ parsed.details
725
+ );
726
+ }
727
+ try {
728
+ return JSON.parse(text);
729
+ } catch {
730
+ throw new VaultFlowClientError(
731
+ step,
732
+ `${path} returned 200 with a non-JSON body`,
733
+ text
734
+ );
735
+ }
347
736
  }
348
- return {
349
- requirement,
350
- amountRawUsdc: BigInt(requirement.amount),
351
- payTo: requirement.payTo,
352
- feePayer
353
- };
354
- }
355
- function standardRequirementMatchesSelected(candidate, selected) {
356
- const parsed = standardExactRequirementSchema.safeParse(candidate);
357
- return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
358
- }
359
- function stableJson(value) {
360
- return JSON.stringify(sortJson(value));
361
- }
362
- function sortJson(value) {
363
- if (Array.isArray(value)) {
364
- return value.map(sortJson);
737
+ async getJson(path) {
738
+ const url = `${this.baseUrl}${path}`;
739
+ const response = await this.fetchImpl(url, {
740
+ headers: await walletAuthHeaders({
741
+ signer: this.signer,
742
+ method: "GET",
743
+ url
744
+ })
745
+ });
746
+ const text = await response.text();
747
+ if (response.status !== 200) {
748
+ const parsed = parseRelayerError(text);
749
+ throw new VaultFlowClientError(
750
+ "read",
751
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
752
+ text,
753
+ parsed.code,
754
+ parsed.details
755
+ );
756
+ }
757
+ try {
758
+ return JSON.parse(text);
759
+ } catch {
760
+ throw new VaultFlowClientError(
761
+ "read",
762
+ `${path} returned 200 with a non-JSON body`,
763
+ text
764
+ );
765
+ }
365
766
  }
366
- if (value !== null && typeof value === "object") {
367
- return Object.fromEntries(
368
- Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
369
- );
767
+ };
768
+ function parseRelayerError(text) {
769
+ try {
770
+ const parsed = JSON.parse(text);
771
+ return {
772
+ code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
773
+ message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
774
+ details: parsed.error?.details ?? null
775
+ };
776
+ } catch {
777
+ return { code: null, message: null, details: null };
370
778
  }
371
- return value;
372
779
  }
373
780
 
374
- // ../../src/client/standard-x402-payer.ts
375
- var StandardX402PayError = class extends Error {
376
- constructor(reason, message, detail = null) {
781
+ // ../../src/client/relayer-yield-realizer.ts
782
+ var REALIZE_OVERHEAD_RAW_USDC = 2500n;
783
+ var RelayerRealizeError = class extends Error {
784
+ constructor(code, message, detail = null) {
377
785
  super(message);
378
- this.reason = reason;
786
+ this.code = code;
379
787
  this.detail = detail;
380
- this.name = "StandardX402PayError";
788
+ this.name = "RelayerRealizeError";
381
789
  }
382
- reason;
790
+ code;
383
791
  detail;
384
792
  };
385
- var StandardX402Payer = class {
386
- realizer;
387
- x402Fetch;
388
- probeFetch;
389
- defaultMaxAmountRawUsdc;
390
- network;
391
- usdcMint;
392
- stateStore;
393
- pending = /* @__PURE__ */ new Map();
394
- inFlight = /* @__PURE__ */ new Map();
395
- nowMs;
396
- constructor(config) {
397
- this.realizer = config.realizer;
398
- this.x402Fetch = config.x402Fetch;
399
- this.probeFetch = config.probeFetch ?? fetch;
400
- this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
401
- this.network = config.network ?? SOLANA_MAINNET_NETWORK;
402
- this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
403
- this.stateStore = config.stateStore ?? null;
404
- this.nowMs = config.nowMs ?? (() => Date.now());
405
- if (this.stateStore !== null) {
406
- for (const record of this.stateStore.load()) {
407
- this.pending.set(record.key, record);
408
- }
409
- }
793
+ var RelayerYieldRealizer = class {
794
+ get vault() {
795
+ return this.vaultFlows.vault.address;
410
796
  }
411
- pay(input) {
412
- const method = (input.method ?? "GET").toUpperCase();
413
- const requestBodyHash = requestBodyHashFor(input.body ?? null);
414
- const pendingKey = pendingPaymentKey({
415
- url: input.url,
416
- method,
417
- requestBodyHash
418
- });
419
- const existingFlow = this.inFlight.get(pendingKey);
420
- if (existingFlow !== void 0) {
421
- return existingFlow;
422
- }
423
- const flow = this.run(input, {
424
- method,
425
- requestBodyHash,
426
- pendingKey
427
- }).finally(() => {
428
- this.inFlight.delete(pendingKey);
797
+ vaultFlows;
798
+ constructor(config) {
799
+ this.vaultFlows = new VaultFlowClient({
800
+ relayerBaseUrl: config.relayerBaseUrl,
801
+ signer: config.signer,
802
+ rpc: config.rpc,
803
+ ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
804
+ ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
429
805
  });
430
- this.inFlight.set(pendingKey, flow);
431
- return flow;
432
806
  }
433
- async run(input, computed) {
434
- const { method, requestBodyHash, pendingKey } = computed;
435
- const existingPending = this.pending.get(pendingKey);
436
- if (existingPending !== void 0 && input.forceNewPayment !== true) {
437
- throw new StandardX402PayError(
438
- "payment_outcome_unknown",
439
- "a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
440
- existingPending
441
- );
442
- }
443
- if (existingPending !== void 0 && input.forceNewPayment === true) {
444
- try {
445
- this.untrack(pendingKey);
446
- } catch (error) {
447
- throw new StandardX402PayError(
448
- "state_persist_failed",
449
- "could not clear the previous pending x402 marker before forcing a new payment",
450
- error
451
- );
452
- }
453
- }
454
- const init = {
455
- method,
456
- ...input.body === void 0 ? {} : { body: input.body },
457
- ...input.headers === void 0 ? {} : { headers: input.headers }
458
- };
459
- const probe = await this.probeFetch(input.url, init);
460
- if (probe.status !== 402) {
461
- return { paid: false, status: probe.status, body: await probe.text() };
462
- }
463
- const selected = await this.selectRequirement(probe);
464
- const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
465
- if (selected.amountRawUsdc > cap) {
466
- throw new StandardX402PayError(
467
- "amount_exceeds_client_cap",
468
- `the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
469
- { amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
470
- );
471
- }
472
- let realized;
807
+ async ensureUsdcAvailable(input) {
808
+ const shortfallRawUsdc = input.amountRawUsdc;
809
+ await this.assertSpendableYield(shortfallRawUsdc);
810
+ let outcome;
473
811
  try {
474
- realized = await this.realizer.ensureUsdcAvailable({
475
- amountRawUsdc: selected.amountRawUsdc,
476
- payment: {
477
- payTo: selected.payTo,
478
- amountRawUsdc: selected.amountRawUsdc.toString(),
479
- resourceUrlHash: sha256HexOf(input.url),
480
- method
481
- },
812
+ outcome = await this.vaultFlows.withdraw({
813
+ amountRawUsdc: shortfallRawUsdc,
814
+ // The relayer refuses to prepare this withdrawal beyond the spendable
815
+ // yield — the principal-protection guard the client cannot bypass.
816
+ purpose: "yield_realize",
817
+ // Declares what is being paid so the relayer's spending-mandate layer
818
+ // can enforce caps/payee and keep the mandate → payment audit chain.
819
+ ...input.payment === void 0 ? {} : { payment: input.payment },
482
820
  ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
483
821
  });
484
822
  } catch (error) {
485
- if (error.code === "approval_required") {
486
- throw new StandardX402PayError(
487
- "approval_required",
488
- "this payment exceeds the owner-approval threshold; NOTHING was paid. Ask the owner to open the approveUrl, then retry the same call with the approvalId",
489
- error.detail ?? null
490
- );
491
- }
492
- throw new StandardX402PayError(
493
- "realize_failed",
494
- `could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
495
- error
496
- );
497
- }
498
- const pendingRecord = {
499
- key: pendingKey,
500
- url: input.url,
501
- method,
502
- requestBodyHash,
503
- amountRawUsdc: selected.amountRawUsdc.toString(),
504
- payTo: selected.payTo,
505
- feePayer: selected.feePayer,
506
- realizedRawUsdc: realized.realizedRawUsdc.toString(),
507
- realizeTxSignature: realized.txSignature,
508
- status: "realized",
509
- createdAtMs: this.nowMs(),
510
- updatedAtMs: this.nowMs()
511
- };
512
- try {
513
- this.track(pendingRecord);
514
- } catch (error) {
515
- throw new StandardX402PayError(
516
- "state_persist_failed",
517
- "could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
518
- { error, pendingPayment: pendingRecord }
519
- );
520
- }
521
- let response;
522
- try {
523
- response = await this.x402Fetch(input.url, init, selected);
524
- } catch (error) {
525
- const persistError = this.tryMarkUnknown(pendingKey, {
526
- message: error instanceof Error ? error.message : String(error)
527
- });
528
- throw new StandardX402PayError(
529
- "payment_outcome_unknown",
530
- `the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
531
- { error, persistError }
532
- );
533
- }
534
- const bodyText = await response.text();
535
- if (response.status !== 200) {
536
- const persistError = this.tryMarkUnknown(pendingKey, {
537
- status: response.status,
538
- body: bodyText
539
- });
540
- throw new StandardX402PayError(
541
- "payment_outcome_unknown",
542
- `the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
543
- { status: response.status, body: bodyText, persistError }
544
- );
545
- }
546
- this.clearDelivered(pendingKey);
547
- const paymentTxSignature = extractSettledPaymentTxSignature(response);
548
- if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && this.realizer.reportPayment !== void 0) {
549
- try {
550
- await this.realizer.reportPayment({
551
- withdrawalId: realized.withdrawalId,
552
- paymentTxSignature
553
- });
554
- } catch (error) {
555
- console.error(
556
- `[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
557
- );
558
- }
823
+ throw this.mapWithdrawError(error);
559
824
  }
560
- return {
561
- paid: true,
562
- status: response.status,
563
- body: bodyText,
564
- payment: {
565
- amountRawUsdc: selected.amountRawUsdc.toString(),
566
- payTo: selected.payTo,
567
- feePayer: selected.feePayer,
568
- realizedRawUsdc: realized.realizedRawUsdc.toString(),
569
- realizeTxSignature: realized.txSignature,
570
- paymentTxSignature
571
- }
825
+ if (outcome.status !== "confirmed" || outcome.txSignature === null) {
826
+ throw new RelayerRealizeError(
827
+ "realize_not_confirmed",
828
+ `yield realize withdrawal did not confirm (status=${outcome.status})`,
829
+ outcome
830
+ );
831
+ }
832
+ return {
833
+ realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
834
+ txSignature: outcome.txSignature,
835
+ withdrawalId: outcome.withdrawalId
572
836
  };
573
837
  }
574
- /** Reads the challenge from the header (preferred) or the JSON body. */
575
- async selectRequirement(probe) {
576
- const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
577
- let requirements;
838
+ /**
839
+ * Best-effort report-back of the x402 payment tx this realize funded —
840
+ * closes the relayer's mandate → realize → payment audit chain. Callers
841
+ * must never let a failure here affect the payment result.
842
+ */
843
+ async reportPayment(input) {
844
+ await this.vaultFlows.reportPayment(input);
845
+ }
846
+ /**
847
+ * Refuses to realize more than the ledger's spendable yield (principal).
848
+ * getBudget syncs the relayer's ledger from chain first (best-effort), so a
849
+ * long-running client sees yield as it accrues instead of a frozen view.
850
+ */
851
+ async assertSpendableYield(shortfallRawUsdc) {
852
+ let spendable;
578
853
  try {
579
- if (header !== null) {
580
- requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
581
- } else {
582
- requirements = parseStandardChallenge(
583
- await probe.json()
584
- ).solanaExactRequirements;
585
- }
854
+ const budget = await this.vaultFlows.getBudget();
855
+ spendable = BigInt(budget.spendableYieldRawUsdc);
586
856
  } catch (error) {
587
- throw new StandardX402PayError(
588
- error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
589
- "could not parse the x402 402 challenge",
857
+ throw new RelayerRealizeError(
858
+ "budget_unavailable",
859
+ "could not read the spendable-yield budget",
590
860
  error
591
861
  );
592
862
  }
593
- try {
594
- return selectPayableSolanaRequirement(requirements, {
595
- network: this.network,
596
- usdcMint: this.usdcMint
597
- });
598
- } catch (error) {
599
- throw new StandardX402PayError(
600
- "no_payable_requirement",
601
- error instanceof Error ? error.message : String(error),
863
+ const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
864
+ if (spendable < requiredRawUsdc) {
865
+ throw new RelayerRealizeError(
866
+ "insufficient_yield",
867
+ `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
868
+ { spendableYieldRawUsdc: spendable.toString() }
869
+ );
870
+ }
871
+ }
872
+ mapWithdrawError(error) {
873
+ if (!(error instanceof VaultFlowClientError)) {
874
+ return new RelayerRealizeError(
875
+ "prepare_failed",
876
+ `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
602
877
  error
603
878
  );
604
879
  }
880
+ const serverCode = error.code ?? errorCodeFrom(error.detail);
881
+ if (serverCode === "approval_required") {
882
+ return new RelayerRealizeError(
883
+ "approval_required",
884
+ "this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
885
+ error.errorDetails ?? error.detail
886
+ );
887
+ }
888
+ if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
889
+ return new RelayerRealizeError(
890
+ "insufficient_yield",
891
+ "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
892
+ error.detail
893
+ );
894
+ }
895
+ return new RelayerRealizeError(
896
+ error.step === "submit" ? "submit_failed" : "prepare_failed",
897
+ error.message,
898
+ error.detail
899
+ );
605
900
  }
606
- track(record) {
607
- const previous = this.pending.get(record.key);
608
- this.pending.set(record.key, record);
609
- try {
610
- this.persist();
611
- } catch (error) {
612
- if (previous === void 0) {
613
- this.pending.delete(record.key);
614
- } else {
615
- this.pending.set(record.key, previous);
901
+ };
902
+ function errorCodeFrom(detail) {
903
+ if (typeof detail !== "string") {
904
+ return null;
905
+ }
906
+ try {
907
+ const parsed = JSON.parse(detail);
908
+ return typeof parsed.error?.code === "string" ? parsed.error.code : null;
909
+ } catch {
910
+ return null;
911
+ }
912
+ }
913
+
914
+ // src/mcp-server.ts
915
+ import { homedir } from "node:os";
916
+ import { join as join2 } from "node:path";
917
+
918
+ // ../../src/client/mcp-payment-server.ts
919
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
920
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
921
+ import {
922
+ CallToolRequestSchema,
923
+ ListToolsRequestSchema
924
+ } from "@modelcontextprotocol/sdk/types.js";
925
+
926
+ // ../../src/client/onboarding.ts
927
+ var SELF_SERVE_POLICY_ID = "self-serve";
928
+ var OnboardingError = class extends Error {
929
+ constructor(step, message, detail = null) {
930
+ super(message);
931
+ this.step = step;
932
+ this.detail = detail;
933
+ this.name = "OnboardingError";
934
+ }
935
+ step;
936
+ detail;
937
+ };
938
+ async function ensureWalletOnboarded(params) {
939
+ const fetchImpl = params.fetchImpl ?? fetch;
940
+ const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
941
+ const post = async (step, path, body) => {
942
+ const url = `${baseUrl}${path}`;
943
+ const serialized = JSON.stringify(body);
944
+ const response = await fetchImpl(url, {
945
+ method: "POST",
946
+ headers: {
947
+ ...await walletAuthHeaders({
948
+ signer: params.signer,
949
+ method: "POST",
950
+ url,
951
+ body: serialized
952
+ }),
953
+ "content-type": "application/json"
954
+ },
955
+ body: serialized
956
+ });
957
+ if (response.status !== 200) {
958
+ let detail = null;
959
+ try {
960
+ detail = await response.json();
961
+ } catch {
962
+ detail = null;
616
963
  }
617
- throw error;
964
+ throw new OnboardingError(
965
+ step,
966
+ `wallet onboarding ${step} failed with ${response.status}`,
967
+ detail
968
+ );
618
969
  }
970
+ };
971
+ const wallet = params.signer.walletAddress;
972
+ const vault = params.vault ?? params.signer.vault?.address ?? SUBLY_VAULT.address;
973
+ await post("register", "/v1/wallets/agent", {
974
+ wallet,
975
+ vault,
976
+ signingPolicyId: SELF_SERVE_POLICY_ID,
977
+ signingMode: "non_interactive",
978
+ signerValidationMode: params.signer.validationMode,
979
+ signerProvider: params.signer.provider ?? "local-keypair",
980
+ activateForPayments: true
981
+ });
982
+ await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain", vault });
983
+ }
984
+
985
+ // ../../src/x402/headers.ts
986
+ import { z as z2 } from "zod";
987
+
988
+ // ../../src/lib/hash.ts
989
+ import { createHash as createHash3 } from "node:crypto";
990
+ var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
991
+ function sha256TaggedHex(data) {
992
+ return `sha256-${createHash3("sha256").update(data).digest("hex")}`;
993
+ }
994
+ function stableStringify(value) {
995
+ if (value === null) {
996
+ return "null";
619
997
  }
620
- markUnknown(key, detail) {
621
- const current = this.pending.get(key);
622
- if (current === void 0) {
623
- return;
624
- }
625
- const next = {
626
- ...current,
627
- status: "external_outcome_unknown",
628
- updatedAtMs: this.nowMs(),
629
- detail
630
- };
631
- this.pending.set(key, next);
632
- try {
633
- this.persist();
634
- } catch (error) {
635
- this.pending.set(key, current);
636
- throw error;
637
- }
998
+ if (typeof value === "bigint") {
999
+ return JSON.stringify(value.toString());
1000
+ }
1001
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1002
+ return JSON.stringify(value);
1003
+ }
1004
+ if (Array.isArray(value)) {
1005
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
1006
+ }
1007
+ const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
1008
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
1009
+ }
1010
+ function hashStableJson(value) {
1011
+ return sha256TaggedHex(stableStringify(value));
1012
+ }
1013
+
1014
+ // ../../src/x402/headers.ts
1015
+ var PAYMENT_REQUIRED_HEADER = "payment-required";
1016
+ var MAX_HEADER_JSON_BYTES = 16384;
1017
+ var X402HeaderError = class extends Error {
1018
+ reason;
1019
+ constructor(reason, message) {
1020
+ super(message);
1021
+ this.name = "X402HeaderError";
1022
+ this.reason = reason;
1023
+ }
1024
+ };
1025
+ var sublyPaymentRequirementsSchema = z2.object({
1026
+ scheme: z2.literal(PAYMENT_SCHEME),
1027
+ network: z2.string().min(1),
1028
+ asset: z2.string().min(32),
1029
+ /** Exact seller amount in raw USDC; the scheme settles exactly this. */
1030
+ amountRawUsdc: z2.string().regex(/^[1-9]\d*$/),
1031
+ resource: z2.string().url(),
1032
+ description: z2.string().optional(),
1033
+ mimeType: z2.string().optional(),
1034
+ payTo: z2.string().min(32),
1035
+ maxTimeoutSeconds: z2.number().int().positive(),
1036
+ extra: z2.object({
1037
+ sellerRequestId: z2.string().min(1),
1038
+ seller: z2.string().min(32),
1039
+ sellerUsdcAta: z2.string().min(32),
1040
+ vault: z2.string().min(32),
1041
+ shareMint: z2.string().min(32)
1042
+ })
1043
+ }).loose();
1044
+ var paymentRequiredSchema = z2.object({
1045
+ x402Version: z2.number().int(),
1046
+ accepts: z2.array(z2.unknown()),
1047
+ error: z2.string().optional()
1048
+ }).loose();
1049
+ var sublyPaymentPayloadSchema = z2.object({
1050
+ x402Version: z2.number().int(),
1051
+ scheme: z2.literal(PAYMENT_SCHEME),
1052
+ network: z2.string().min(1),
1053
+ payload: z2.object({
1054
+ paymentId: z2.string().min(1),
1055
+ requestBindingHash: z2.string().min(1),
1056
+ preparedMessageHash: z2.string().min(1),
1057
+ serializedTransaction: z2.string().min(1).max(4096),
1058
+ agentSignature: z2.string().min(1).max(128),
1059
+ temporarySettlementSignature: z2.string().min(1).max(128)
1060
+ })
1061
+ }).loose();
1062
+ function decodeX402Header(headerValue) {
1063
+ if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
1064
+ throw new X402HeaderError(
1065
+ "header_too_large",
1066
+ `x402 header exceeds ${MAX_HEADER_JSON_BYTES} encoded bytes`
1067
+ );
1068
+ }
1069
+ const json = Buffer.from(headerValue, "base64").toString("utf8");
1070
+ try {
1071
+ return JSON.parse(json);
1072
+ } catch {
1073
+ throw new X402HeaderError(
1074
+ "invalid_header_encoding",
1075
+ "x402 header is not base64-encoded JSON"
1076
+ );
638
1077
  }
639
- tryMarkUnknown(key, detail) {
640
- try {
641
- this.markUnknown(key, detail);
642
- return null;
643
- } catch (error) {
644
- return error;
645
- }
1078
+ }
1079
+ function requestBodyHashFor(body) {
1080
+ if (body === null || body === void 0 || body.length === 0) {
1081
+ return EMPTY_BODY_HASH;
646
1082
  }
647
- untrack(key) {
648
- const previous = this.pending.get(key);
649
- const existed = previous !== void 0;
650
- this.pending.delete(key);
651
- try {
652
- this.persist();
653
- } catch (error) {
654
- if (existed) {
655
- this.pending.set(key, previous);
656
- }
657
- throw error;
658
- }
1083
+ return sha256TaggedHex(
1084
+ typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1085
+ );
1086
+ }
1087
+
1088
+ // ../../src/client/paid-fetch.ts
1089
+ function formatRawUsdcAmount(raw) {
1090
+ const value = BigInt(raw);
1091
+ const negative = value < 0n;
1092
+ const abs = negative ? -value : value;
1093
+ const whole = abs / 1000000n;
1094
+ const frac = (abs % 1000000n).toString().padStart(6, "0");
1095
+ return `${negative ? "-" : ""}${whole}.${frac}`;
1096
+ }
1097
+
1098
+ // ../../src/lib/canonical-json.ts
1099
+ import { createHash as createHash4 } from "node:crypto";
1100
+ function sha256HexOf(data) {
1101
+ return createHash4("sha256").update(data, "utf8").digest("hex");
1102
+ }
1103
+
1104
+ // ../../src/x402/standard-requirements.ts
1105
+ import { z as z3 } from "zod";
1106
+ var STANDARD_EXACT_SCHEME = "exact";
1107
+ var standardExactRequirementSchema = z3.object({
1108
+ scheme: z3.literal(STANDARD_EXACT_SCHEME),
1109
+ /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
1110
+ network: z3.string().min(1),
1111
+ /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
1112
+ asset: z3.string().min(1),
1113
+ /** Exact price in the asset's atomic units, as a decimal string. */
1114
+ amount: z3.string().regex(/^[1-9]\d*$/),
1115
+ /** Recipient wallet; the transfer destination ATA is derived from it. */
1116
+ payTo: z3.string().min(1),
1117
+ maxTimeoutSeconds: z3.number().int().positive().optional(),
1118
+ extra: z3.object({
1119
+ /** Facilitator address that pays the tx fee (gas sponsorship). */
1120
+ feePayer: z3.string().min(1).optional()
1121
+ }).loose().optional()
1122
+ }).loose();
1123
+ var standardPaymentRequiredSchema = z3.object({
1124
+ x402Version: z3.number().int(),
1125
+ accepts: z3.array(z3.unknown()),
1126
+ error: z3.string().optional(),
1127
+ resource: z3.object({ url: z3.string().optional() }).loose().optional()
1128
+ }).loose();
1129
+ var StandardX402ChallengeError = class extends Error {
1130
+ reason;
1131
+ constructor(reason, message) {
1132
+ super(message);
1133
+ this.name = "StandardX402ChallengeError";
1134
+ this.reason = reason;
659
1135
  }
660
- clearDelivered(key) {
661
- const previous = this.pending.get(key);
662
- this.pending.delete(key);
663
- try {
664
- this.persist();
665
- } catch (error) {
666
- if (previous !== void 0) {
667
- this.pending.set(key, previous);
668
- }
669
- console.error(
670
- `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
671
- );
672
- }
1136
+ };
1137
+ function parseStandardChallenge(challenge) {
1138
+ const parsed = standardPaymentRequiredSchema.safeParse(challenge);
1139
+ if (!parsed.success) {
1140
+ throw new StandardX402ChallengeError(
1141
+ "invalid_payment_required",
1142
+ "Response is not a valid x402 PaymentRequired object"
1143
+ );
673
1144
  }
674
- persist() {
675
- if (this.stateStore === null) {
676
- return;
1145
+ const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
1146
+ const requirement = standardExactRequirementSchema.safeParse(candidate);
1147
+ if (!requirement.success) {
1148
+ return [];
677
1149
  }
678
- this.stateStore.save([...this.pending.values()]);
679
- }
680
- };
681
- function pendingPaymentKey(input) {
682
- return `${input.method}:${input.url}:${input.requestBodyHash}`;
1150
+ return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
1151
+ });
1152
+ return { paymentRequired: parsed.data, solanaExactRequirements };
683
1153
  }
684
- function extractSettledPaymentTxSignature(response) {
685
- const header = response.headers.get("x-payment-response");
686
- if (header === null || header.length === 0) {
687
- return null;
688
- }
1154
+ function decodeStandardPaymentRequiredHeader(headerValue) {
1155
+ let decoded;
689
1156
  try {
690
- const decoded = JSON.parse(
691
- Buffer.from(header, "base64").toString("utf8")
1157
+ decoded = decodeX402Header(headerValue);
1158
+ } catch (error) {
1159
+ throw new StandardX402ChallengeError(
1160
+ error instanceof X402HeaderError ? error.reason : "invalid_header",
1161
+ "Cannot decode the payment-required header"
692
1162
  );
693
- if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
694
- return decoded.transaction;
695
- }
696
- if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
697
- return decoded.txHash;
698
- }
699
- return null;
700
- } catch {
701
- return null;
702
1163
  }
1164
+ return parseStandardChallenge(decoded);
703
1165
  }
704
-
705
- // ../../src/client/lookup-tables.ts
706
- import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
707
- import { address, getCompiledTransactionMessageDecoder } from "@solana/kit";
708
- function lookupTableAddressesForTransaction(serializedTransaction) {
709
- const wire = Buffer.from(serializedTransaction, "base64");
710
- let offset = 0;
711
- let signatureCount = 0;
712
- let shift = 0;
713
- while (offset < wire.length) {
714
- const byte = wire[offset];
715
- signatureCount |= (byte & 127) << shift;
716
- offset += 1;
717
- if ((byte & 128) === 0) {
718
- break;
719
- }
720
- shift += 7;
1166
+ function selectPayableSolanaRequirement(requirements, options) {
1167
+ const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1168
+ const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1169
+ const matchingRequirements = requirements.filter(
1170
+ (candidate) => candidate.network === network && candidate.asset === usdcMint
1171
+ );
1172
+ if (matchingRequirements.length === 0) {
1173
+ throw new StandardX402ChallengeError(
1174
+ "no_payable_requirement",
1175
+ `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1176
+ );
721
1177
  }
722
- const messageBytes = wire.subarray(offset + signatureCount * 64);
723
- const compiled = getCompiledTransactionMessageDecoder().decode(messageBytes);
724
- const lookups = compiled.addressTableLookups ?? [];
725
- return lookups.map((lookup) => String(lookup.lookupTableAddress));
1178
+ const requirement = matchingRequirements.find(
1179
+ (candidate) => candidate.extra?.feePayer !== void 0
1180
+ ) ?? null;
1181
+ if (requirement === null) {
1182
+ throw new StandardX402ChallengeError(
1183
+ "missing_svm_fee_payer",
1184
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1185
+ );
1186
+ }
1187
+ const feePayer = requirement.extra?.feePayer;
1188
+ if (feePayer === void 0) {
1189
+ throw new StandardX402ChallengeError(
1190
+ "missing_svm_fee_payer",
1191
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1192
+ );
1193
+ }
1194
+ return {
1195
+ requirement,
1196
+ amountRawUsdc: BigInt(requirement.amount),
1197
+ payTo: requirement.payTo,
1198
+ feePayer
1199
+ };
726
1200
  }
727
- async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
728
- const addresses = lookupTableAddressesForTransaction(serializedTransaction);
729
- if (addresses.length === 0) {
730
- return {};
1201
+ function standardRequirementMatchesSelected(candidate, selected2) {
1202
+ const parsed = standardExactRequirementSchema.safeParse(candidate);
1203
+ return parsed.success && stableJson(parsed.data) === stableJson(selected2.requirement);
1204
+ }
1205
+ function stableJson(value) {
1206
+ return JSON.stringify(sortJson(value));
1207
+ }
1208
+ function sortJson(value) {
1209
+ if (Array.isArray(value)) {
1210
+ return value.map(sortJson);
731
1211
  }
732
- const tables = await fetchAllMaybeAddressLookupTable(
733
- rpc2,
734
- addresses.map((value) => address(value))
735
- );
736
- const result = {};
737
- for (const table of tables) {
738
- if (table.exists) {
739
- result[table.address] = table.data.addresses.map(String);
740
- }
1212
+ if (value !== null && typeof value === "object") {
1213
+ return Object.fromEntries(
1214
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
1215
+ );
741
1216
  }
742
- return result;
1217
+ return value;
743
1218
  }
744
1219
 
745
- // ../../src/client/vault-flows.ts
746
- var VaultFlowClientError = class extends Error {
747
- constructor(step, message, detail = null, code = null, errorDetails = null) {
1220
+ // ../../src/client/standard-x402-payer.ts
1221
+ var StandardX402PayError = class extends Error {
1222
+ constructor(reason, message, detail = null) {
748
1223
  super(message);
749
- this.step = step;
750
- this.detail = detail;
751
- this.code = code;
752
- this.errorDetails = errorDetails;
753
- this.name = "VaultFlowClientError";
754
- }
755
- step;
756
- detail;
757
- code;
758
- errorDetails;
759
- };
760
- var VaultFlowClient = class {
761
- baseUrl;
762
- signer;
763
- fetchImpl;
764
- lookupTablesFor;
765
- pollTimeoutMs;
766
- pollIntervalMs;
767
- constructor(config) {
768
- this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
769
- this.signer = config.signer;
770
- this.fetchImpl = config.fetchImpl ?? fetch;
771
- this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
772
- this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
773
- this.pollIntervalMs = config.pollIntervalMs ?? 2500;
774
- }
775
- /**
776
- * Moves USDC from the agent wallet into the vault (fee sponsored). Under
777
- * depositPolicy "owner_approval_required" the relayer refuses to prepare
778
- * without an owner approval; when the caller passes none, an already
779
- * APPROVED deposit approval for this exact amount (e.g. the mandate's
780
- * initialDeposit — "one Face ID covers mandate + first deposit") is looked
781
- * up and used automatically before surfacing deposit_approval_required.
782
- */
783
- async deposit(input) {
784
- let approvalId = input.approvalId;
785
- let prepared;
786
- try {
787
- prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
788
- wallet: this.signer.walletAddress,
789
- amountRawUsdc: input.amountRawUsdc.toString(),
790
- ...approvalId === void 0 ? {} : { approvalId }
791
- });
792
- } catch (error) {
793
- if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
794
- throw error;
795
- }
796
- approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
797
- if (approvalId === void 0) {
798
- throw error;
1224
+ this.reason = reason;
1225
+ this.detail = detail;
1226
+ this.name = "StandardX402PayError";
1227
+ }
1228
+ reason;
1229
+ detail;
1230
+ };
1231
+ var StandardX402Payer = class {
1232
+ realizer;
1233
+ x402Fetch;
1234
+ probeFetch;
1235
+ defaultMaxAmountRawUsdc;
1236
+ network;
1237
+ usdcMint;
1238
+ stateStore;
1239
+ pending = /* @__PURE__ */ new Map();
1240
+ inFlight = /* @__PURE__ */ new Map();
1241
+ nowMs;
1242
+ constructor(config) {
1243
+ this.realizer = config.realizer;
1244
+ this.x402Fetch = config.x402Fetch;
1245
+ this.probeFetch = config.probeFetch ?? fetch;
1246
+ this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1247
+ this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1248
+ this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1249
+ this.stateStore = config.stateStore ?? null;
1250
+ this.nowMs = config.nowMs ?? (() => Date.now());
1251
+ if (this.stateStore !== null) {
1252
+ for (const record of this.stateStore.load()) {
1253
+ this.pending.set(record.key, record);
799
1254
  }
800
- prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
801
- wallet: this.signer.walletAddress,
802
- amountRawUsdc: input.amountRawUsdc.toString(),
803
- approvalId
804
- });
805
1255
  }
806
- const signed = await this.signer.signDeposit({
807
- intent: prepared.signingIntent,
808
- serializedTransaction: prepared.serializedTransaction,
809
- lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1256
+ }
1257
+ pay(input, realizer = this.realizer) {
1258
+ const method = (input.method ?? "GET").toUpperCase();
1259
+ const requestBodyHash = requestBodyHashFor(input.body ?? null);
1260
+ const pendingKey = pendingPaymentKey({
1261
+ url: input.url,
1262
+ method,
1263
+ requestBodyHash
810
1264
  });
811
- let outcome = await this.postJson("submit", "/v1/deposits/submit", {
812
- depositId: prepared.depositId,
813
- serializedTransaction: signed.serializedTransaction,
814
- agentSignature: signed.agentSignature
1265
+ const existingFlow = this.inFlight.get(pendingKey);
1266
+ if (existingFlow !== void 0) {
1267
+ return existingFlow;
1268
+ }
1269
+ const run = async () => {
1270
+ if (this.stateStore?.withExclusiveLock) {
1271
+ this.pending.clear();
1272
+ for (const record of this.stateStore.load()) this.pending.set(record.key, record);
1273
+ }
1274
+ return this.run(input, { method, requestBodyHash, pendingKey }, realizer);
1275
+ };
1276
+ const flow = (this.stateStore?.withExclusiveLock ? this.stateStore.withExclusiveLock(run) : run()).finally(() => {
1277
+ this.inFlight.delete(pendingKey);
815
1278
  });
816
- if (outcome.status === "submitted") {
817
- outcome = await this.pollUntilTerminal(
818
- `/v1/deposits/${prepared.depositId}`,
819
- outcome
1279
+ this.inFlight.set(pendingKey, flow);
1280
+ return flow;
1281
+ }
1282
+ async run(input, computed, realizer) {
1283
+ const { method, requestBodyHash, pendingKey } = computed;
1284
+ const existingPending = this.pending.get(pendingKey);
1285
+ if (existingPending !== void 0 && input.forceNewPayment !== true) {
1286
+ throw new StandardX402PayError(
1287
+ "payment_outcome_unknown",
1288
+ "a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
1289
+ existingPending
820
1290
  );
821
1291
  }
822
- return {
823
- depositId: prepared.depositId,
824
- status: outcome.status,
825
- txSignature: outcome.txSignature ?? null,
826
- actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
827
- sharesMintedRaw: outcome.sharesMintedRaw ?? null,
828
- errorCode: outcome.errorCode ?? null
1292
+ if (existingPending !== void 0 && input.forceNewPayment === true) {
1293
+ try {
1294
+ this.untrack(pendingKey);
1295
+ } catch (error) {
1296
+ throw new StandardX402PayError(
1297
+ "state_persist_failed",
1298
+ "could not clear the previous pending x402 marker before forcing a new payment",
1299
+ error
1300
+ );
1301
+ }
1302
+ }
1303
+ const init = {
1304
+ method,
1305
+ ...input.body === void 0 ? {} : { body: input.body },
1306
+ ...input.headers === void 0 ? {} : { headers: input.headers }
829
1307
  };
830
- }
831
- /**
832
- * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
833
- * sponsored). A plain withdrawal is the exit path and MAY spend principal;
834
- * with purpose "yield_realize" the relayer refuses anything beyond the
835
- * spendable yield (the payment path, via RelayerYieldRealizer).
836
- */
837
- async withdraw(input) {
838
- const prepared = await this.postJson(
839
- "prepare",
840
- "/v1/withdrawals/prepare",
841
- {
842
- wallet: this.signer.walletAddress,
843
- amountRawUsdc: input.amountRawUsdc.toString(),
844
- ...input.purpose === void 0 ? {} : { purpose: input.purpose },
845
- ...input.payment === void 0 ? {} : { payment: input.payment },
1308
+ const probe = await this.probeFetch(input.url, init);
1309
+ if (probe.status !== 402) {
1310
+ return { paid: false, status: probe.status, body: await probe.text() };
1311
+ }
1312
+ const selected2 = await this.selectRequirement(probe);
1313
+ const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1314
+ if (selected2.amountRawUsdc > cap) {
1315
+ throw new StandardX402PayError(
1316
+ "amount_exceeds_client_cap",
1317
+ `the challenge demands ${selected2.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
1318
+ { amountRawUsdc: selected2.amountRawUsdc.toString(), payTo: selected2.payTo }
1319
+ );
1320
+ }
1321
+ let realized;
1322
+ try {
1323
+ realized = await realizer.ensureUsdcAvailable({
1324
+ amountRawUsdc: selected2.amountRawUsdc,
1325
+ payment: {
1326
+ payTo: selected2.payTo,
1327
+ amountRawUsdc: selected2.amountRawUsdc.toString(),
1328
+ resourceUrlHash: sha256HexOf(input.url),
1329
+ method
1330
+ },
846
1331
  ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1332
+ });
1333
+ } catch (error) {
1334
+ if (error.code === "approval_required") {
1335
+ throw new StandardX402PayError(
1336
+ "approval_required",
1337
+ "this payment exceeds the owner-approval threshold; NOTHING was paid. Ask the owner to open the approveUrl, then retry the same call with the approvalId",
1338
+ error.detail ?? null
1339
+ );
847
1340
  }
848
- );
849
- const signed = await this.signer.signWithdrawal({
850
- intent: prepared.signingIntent,
851
- serializedTransaction: prepared.serializedTransaction,
852
- lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
853
- });
854
- let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
855
- withdrawalId: prepared.withdrawalId,
856
- serializedTransaction: signed.serializedTransaction,
857
- agentSignature: signed.agentSignature
858
- });
859
- if (outcome.status === "submitted") {
860
- outcome = await this.pollUntilTerminal(
861
- `/v1/withdrawals/${prepared.withdrawalId}`,
862
- outcome
1341
+ throw new StandardX402PayError(
1342
+ "realize_failed",
1343
+ `could not realize yield to cover ${selected2.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
1344
+ error
863
1345
  );
864
1346
  }
865
- return {
866
- withdrawalId: prepared.withdrawalId,
867
- status: outcome.status,
868
- txSignature: outcome.txSignature ?? null,
869
- destinationUsdcAta: prepared.destinationUsdcAta,
870
- actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
871
- actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
872
- errorCode: outcome.errorCode ?? null
1347
+ const pendingRecord = {
1348
+ key: pendingKey,
1349
+ url: input.url,
1350
+ method,
1351
+ requestBodyHash,
1352
+ amountRawUsdc: selected2.amountRawUsdc.toString(),
1353
+ payTo: selected2.payTo,
1354
+ feePayer: selected2.feePayer,
1355
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1356
+ realizeTxSignature: realized.txSignature,
1357
+ status: "realized",
1358
+ createdAtMs: this.nowMs(),
1359
+ updatedAtMs: this.nowMs()
873
1360
  };
874
- }
875
- /**
876
- * Reads the yield budget. Syncs the relayer's ledger from chain first (so
877
- * yield accrued since the last sync shows up); the sync is best-effort and
878
- * on failure the last-synced view is returned.
879
- */
880
- async getBudget(options = {}) {
881
- if (options.refreshFromChain !== false) {
1361
+ try {
1362
+ this.track(pendingRecord);
1363
+ } catch (error) {
1364
+ throw new StandardX402PayError(
1365
+ "state_persist_failed",
1366
+ "could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
1367
+ { error, pendingPayment: pendingRecord }
1368
+ );
1369
+ }
1370
+ let response;
1371
+ try {
1372
+ response = await this.x402Fetch(input.url, init, selected2);
1373
+ } catch (error) {
1374
+ const persistError = this.tryMarkUnknown(pendingKey, {
1375
+ message: error instanceof Error ? error.message : String(error)
1376
+ });
1377
+ throw new StandardX402PayError(
1378
+ "payment_outcome_unknown",
1379
+ `the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
1380
+ { error, persistError }
1381
+ );
1382
+ }
1383
+ const bodyText = await response.text();
1384
+ if (response.status !== 200) {
1385
+ const persistError = this.tryMarkUnknown(pendingKey, {
1386
+ status: response.status,
1387
+ body: bodyText
1388
+ });
1389
+ throw new StandardX402PayError(
1390
+ "payment_outcome_unknown",
1391
+ `the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
1392
+ { status: response.status, body: bodyText, persistError }
1393
+ );
1394
+ }
1395
+ this.clearDelivered(pendingKey);
1396
+ const paymentTxSignature = extractSettledPaymentTxSignature(response);
1397
+ if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && realizer.reportPayment !== void 0) {
882
1398
  try {
883
- await this.postJson(
884
- "sync",
885
- `/v1/wallets/${this.signer.walletAddress}/sync`,
886
- { source: "chain" }
1399
+ await realizer.reportPayment({
1400
+ withdrawalId: realized.withdrawalId,
1401
+ paymentTxSignature
1402
+ });
1403
+ } catch (error) {
1404
+ console.error(
1405
+ `[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
887
1406
  );
888
- } catch {
889
1407
  }
890
1408
  }
891
- const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
892
- const response = await this.fetchImpl(url, {
893
- headers: await walletAuthHeaders({
894
- signer: this.signer,
895
- method: "GET",
896
- url
897
- })
898
- });
899
- const text = await response.text();
900
- if (response.status !== 200) {
901
- throw new VaultFlowClientError(
902
- "budget",
903
- `budget endpoint returned ${response.status}: ${text}`
1409
+ return {
1410
+ paid: true,
1411
+ ...realizer.vault === void 0 ? {} : { fundingVault: realizer.vault },
1412
+ status: response.status,
1413
+ body: bodyText,
1414
+ payment: {
1415
+ amountRawUsdc: selected2.amountRawUsdc.toString(),
1416
+ payTo: selected2.payTo,
1417
+ feePayer: selected2.feePayer,
1418
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1419
+ realizeTxSignature: realized.txSignature,
1420
+ paymentTxSignature
1421
+ }
1422
+ };
1423
+ }
1424
+ /** Reads the challenge from the header (preferred) or the JSON body. */
1425
+ async selectRequirement(probe) {
1426
+ const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
1427
+ let requirements;
1428
+ try {
1429
+ if (header !== null) {
1430
+ requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
1431
+ } else {
1432
+ requirements = parseStandardChallenge(
1433
+ await probe.json()
1434
+ ).solanaExactRequirements;
1435
+ }
1436
+ } catch (error) {
1437
+ throw new StandardX402PayError(
1438
+ error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
1439
+ "could not parse the x402 402 challenge",
1440
+ error
904
1441
  );
905
1442
  }
906
- let parsed;
907
1443
  try {
908
- parsed = JSON.parse(text);
909
- } catch {
910
- throw new VaultFlowClientError(
911
- "budget",
912
- "budget endpoint returned 200 with a non-JSON body",
913
- text
1444
+ return selectPayableSolanaRequirement(requirements, {
1445
+ network: this.network,
1446
+ usdcMint: this.usdcMint
1447
+ });
1448
+ } catch (error) {
1449
+ throw new StandardX402PayError(
1450
+ "no_payable_requirement",
1451
+ error instanceof Error ? error.message : String(error),
1452
+ error
914
1453
  );
915
1454
  }
916
- const body = parsed;
917
- return {
918
- wallet: this.signer.walletAddress,
919
- principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
920
- positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
921
- grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
922
- spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
923
- };
924
- }
925
- /** Best-effort audit link: reports the x402 payment tx a realize funded. */
926
- async reportPayment(input) {
927
- await this.postJson("submit", "/v1/payments/report", {
928
- wallet: this.signer.walletAddress,
929
- withdrawalId: input.withdrawalId,
930
- paymentTxSignature: input.paymentTxSignature
931
- });
932
- }
933
- /** Wallet's approvals as the relayer sees them (optionally by status). */
934
- async listApprovals(status) {
935
- const body = await this.getJson(
936
- `/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
937
- );
938
- return body.approvals ?? [];
939
1455
  }
940
- /**
941
- * Creates the owner-onboarding setup link (wallet-auth pins the agreed
942
- * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
943
- */
944
- async createSetupSession(input) {
945
- return await this.postJson(
946
- "prepare",
947
- `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
948
- {
949
- ...input.policy === void 0 ? {} : { policy: input.policy },
950
- ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
951
- ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
952
- ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1456
+ track(record) {
1457
+ const previous = this.pending.get(record.key);
1458
+ this.pending.set(record.key, record);
1459
+ try {
1460
+ this.persist();
1461
+ } catch (error) {
1462
+ if (previous === void 0) {
1463
+ this.pending.delete(record.key);
1464
+ } else {
1465
+ this.pending.set(record.key, previous);
953
1466
  }
954
- );
1467
+ throw error;
1468
+ }
955
1469
  }
956
- /** Polls a setup session (public capability URL — no auth needed). */
957
- async getSetupSession(sessionId) {
958
- const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
959
- const response = await this.fetchImpl(url);
960
- const text = await response.text();
961
- if (response.status !== 200) {
962
- const parsed = parseRelayerError(text);
963
- throw new VaultFlowClientError(
964
- "read",
965
- parsed.message ?? `setup session read failed with ${response.status}`,
966
- text,
967
- parsed.code,
968
- parsed.details
969
- );
1470
+ markUnknown(key, detail) {
1471
+ const current = this.pending.get(key);
1472
+ if (current === void 0) {
1473
+ return;
1474
+ }
1475
+ const next = {
1476
+ ...current,
1477
+ status: "external_outcome_unknown",
1478
+ updatedAtMs: this.nowMs(),
1479
+ detail
1480
+ };
1481
+ this.pending.set(key, next);
1482
+ try {
1483
+ this.persist();
1484
+ } catch (error) {
1485
+ this.pending.set(key, current);
1486
+ throw error;
970
1487
  }
971
- return JSON.parse(text);
972
1488
  }
973
- /**
974
- * Finds an APPROVED, unconsumed deposit approval bound to exactly this
975
- * amount — the shape the mandate's initialDeposit approval has.
976
- */
977
- async findApprovedDepositApproval(amountRawUsdc) {
1489
+ tryMarkUnknown(key, detail) {
978
1490
  try {
979
- const approvals = await this.listApprovals("approved");
980
- const match = approvals.find((approval) => {
981
- const binding = approval.binding;
982
- return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
983
- });
984
- return match?.approvalId;
985
- } catch {
986
- return void 0;
1491
+ this.markUnknown(key, detail);
1492
+ return null;
1493
+ } catch (error) {
1494
+ return error;
987
1495
  }
988
1496
  }
989
- /**
990
- * Polls the reconciling GET endpoint until the intent leaves "submitted"
991
- * (each read looks the tx up on-chain) or the timeout elapses.
992
- */
993
- async pollUntilTerminal(path, last) {
994
- const deadline = Date.now() + this.pollTimeoutMs;
995
- let latest = last;
996
- while (Date.now() < deadline) {
997
- await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
998
- const url = `${this.baseUrl}${path}`;
999
- const response = await this.fetchImpl(url, {
1000
- headers: await walletAuthHeaders({
1001
- signer: this.signer,
1002
- method: "GET",
1003
- url
1004
- })
1005
- });
1006
- if (response.status !== 200) {
1007
- continue;
1008
- }
1009
- try {
1010
- latest = await response.json();
1011
- } catch {
1012
- continue;
1013
- }
1014
- if (latest.status !== "submitted") {
1015
- return latest;
1497
+ untrack(key) {
1498
+ const previous = this.pending.get(key);
1499
+ const existed = previous !== void 0;
1500
+ this.pending.delete(key);
1501
+ try {
1502
+ this.persist();
1503
+ } catch (error) {
1504
+ if (existed) {
1505
+ this.pending.set(key, previous);
1016
1506
  }
1507
+ throw error;
1017
1508
  }
1018
- return latest;
1019
1509
  }
1020
- async postJson(step, path, body) {
1021
- const url = `${this.baseUrl}${path}`;
1022
- const serialized = JSON.stringify(body);
1023
- const response = await this.fetchImpl(url, {
1024
- method: "POST",
1025
- headers: {
1026
- ...await walletAuthHeaders({
1027
- signer: this.signer,
1028
- method: "POST",
1029
- url,
1030
- body: serialized
1031
- }),
1032
- "content-type": "application/json"
1033
- },
1034
- body: serialized
1035
- });
1036
- const text = await response.text();
1037
- if (response.status !== 200) {
1038
- const parsed = parseRelayerError(text);
1039
- throw new VaultFlowClientError(
1040
- step,
1041
- parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1042
- text,
1043
- parsed.code,
1044
- parsed.details
1045
- );
1046
- }
1510
+ clearDelivered(key) {
1511
+ const previous = this.pending.get(key);
1512
+ this.pending.delete(key);
1047
1513
  try {
1048
- return JSON.parse(text);
1049
- } catch {
1050
- throw new VaultFlowClientError(
1051
- step,
1052
- `${path} returned 200 with a non-JSON body`,
1053
- text
1514
+ this.persist();
1515
+ } catch (error) {
1516
+ if (previous !== void 0) {
1517
+ this.pending.set(key, previous);
1518
+ }
1519
+ console.error(
1520
+ `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
1054
1521
  );
1055
1522
  }
1056
1523
  }
1057
- async getJson(path) {
1058
- const url = `${this.baseUrl}${path}`;
1059
- const response = await this.fetchImpl(url, {
1060
- headers: await walletAuthHeaders({
1061
- signer: this.signer,
1062
- method: "GET",
1063
- url
1064
- })
1065
- });
1066
- const text = await response.text();
1067
- if (response.status !== 200) {
1068
- const parsed = parseRelayerError(text);
1069
- throw new VaultFlowClientError(
1070
- "read",
1071
- parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1072
- text,
1073
- parsed.code,
1074
- parsed.details
1075
- );
1076
- }
1077
- try {
1078
- return JSON.parse(text);
1079
- } catch {
1080
- throw new VaultFlowClientError(
1081
- "read",
1082
- `${path} returned 200 with a non-JSON body`,
1083
- text
1084
- );
1524
+ persist() {
1525
+ if (this.stateStore === null) {
1526
+ return;
1085
1527
  }
1528
+ this.stateStore.save([...this.pending.values()]);
1086
1529
  }
1087
1530
  };
1088
- function parseRelayerError(text) {
1531
+ function pendingPaymentKey(input) {
1532
+ return `${input.method}:${input.url}:${input.requestBodyHash}`;
1533
+ }
1534
+ function extractSettledPaymentTxSignature(response) {
1535
+ const header = response.headers.get("x-payment-response");
1536
+ if (header === null || header.length === 0) {
1537
+ return null;
1538
+ }
1089
1539
  try {
1090
- const parsed = JSON.parse(text);
1091
- return {
1092
- code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
1093
- message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
1094
- details: parsed.error?.details ?? null
1095
- };
1540
+ const decoded = JSON.parse(
1541
+ Buffer.from(header, "base64").toString("utf8")
1542
+ );
1543
+ if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
1544
+ return decoded.transaction;
1545
+ }
1546
+ if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
1547
+ return decoded.txHash;
1548
+ }
1549
+ return null;
1096
1550
  } catch {
1097
- return { code: null, message: null, details: null };
1551
+ return null;
1098
1552
  }
1099
1553
  }
1100
1554
 
@@ -1105,19 +1559,19 @@ var WITHDRAW_TOOL_NAME = "withdraw_from_subly_vault";
1105
1559
  var BUDGET_TOOL_NAME = "get_subly_yield_budget";
1106
1560
  var SETUP_TOOL_NAME = "create_subly_setup_link";
1107
1561
  var SETUP_STATUS_TOOL_NAME = "check_subly_setup";
1108
- var SERVER_INSTRUCTIONS = `Subly lets an agent pay standard x402 (HTTP 402) paid APIs that offer a Solana USDC exact rail with facilitator feePayer support from its wallet's Kamino vault YIELD \u2014 the deposited principal is never spent, and the seller needs no Subly integration.
1562
+ var SERVER_INSTRUCTIONS = `Subly lets an agent pay standard x402 (HTTP 402) paid APIs that offer a Solana USDC exact rail with facilitator feePayer support from its wallet's Kamino vault YIELD \u2014 the relayer limits API spending to recorded yield, and the seller needs no Subly integration. With a configured vault catalog, call list_subly_vaults and select_subly_vault(vaultAddress) for the user's choice before owner setup. All subsequent tools use that vault until changed. Selection never moves existing funds; set up a separate mandate for each vault. Never select or switch vaults automatically based on APY.
1109
1563
 
1110
- One-time setup: the operator needs a Solana agent wallet. Subly does NOT create wallets; either make a local keypair with \`solana-keygen new -o agent.json\` (or export one from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it, or use a custody wallet \u2014 set SUBLY_SIGNER_PROVIDER=circle (Circle developer-controlled wallet: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ID) or =privy (Privy server wallet incl. agentic/owner-key wallets: PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_WALLET_ID, plus PRIVY_AUTHORIZATION_KEY for owner-key wallets). With a local keypair the private key never leaves that file; with a custody provider it never enters this machine at all. Then fund the wallet with USDC on Solana mainnet \u2014 no SOL is ever needed, all vault transaction fees are sponsored.
1564
+ One-time setup: the operator needs a Solana agent wallet. Subly does NOT create wallets; either make a local keypair with \`solana-keygen new -o agent.json\` (or export one from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it, or use a custody wallet \u2014 set SUBLY_SIGNER_PROVIDER=circle (Circle developer-controlled wallet: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ID) or =privy (Privy server wallet incl. agentic/owner-key wallets: PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_WALLET_ID, plus PRIVY_AUTHORIZATION_KEY for owner-key wallets). With a local keypair the private key never leaves that file; with a custody provider it never enters this machine at all. Then fund the wallet with USDC on Solana mainnet \u2014 a funded relayer sponsors vault transaction fees, which do not require agent SOL.
1111
1565
 
1112
1566
  Owner (human) onboarding: deposits require the human owner's approval (Face ID / wallet signature). During the first deposit conversation, agree the spending limits and the first deposit amount in chat, then call create_subly_setup_link and paste the returned setupUrl to the user AS IS (it expires in 10 minutes). The human opens it on their phone, reviews, and confirms once \u2014 that single confirmation activates the spending mandate AND pre-approves the first deposit. Poll check_subly_setup(sessionId) after the user says they finished, then call deposit_to_subly_vault (the pre-approved first deposit is picked up automatically).
1113
1567
 
1114
1568
  From there the agent can do everything with these tools:
1115
- 1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (minimum just over 1 USDC, e.g. 1010000 raw) so it starts earning yield. If it returns approvalRequired, paste the approveUrl to the user and retry with the approvalId after they approve; if it returns setupRequired, run the owner onboarding above first.
1569
+ 1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (the minimum depends on the selected vault) so it starts earning yield. If it returns approvalRequired, paste the approveUrl to the user and retry with the approvalId after they approve; if it returns setupRequired, run the owner onboarding above first.
1116
1570
  2. get_subly_yield_budget() shows the principal, position value, and the spendable yield a payment can use right now.
1117
1571
  3. fetch_with_subly_payment(url) GETs or POSTs a paid resource from a compatible x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's Solana USDC exact challenge, returning the body plus the payment details. If it returns insufficient_yield, that is expected \u2014 yield accrues over time; wait, do not loop. If it returns approvalRequired (payment above the owner's threshold; NOTHING was paid), paste the approveUrl to the user, and once they say they approved, repeat the SAME call adding the approvalId.
1118
1572
  4. withdraw_from_subly_vault(amountRawUsdc) exits: moves vault funds (principal included) back to the agent wallet's USDC account. If the owner's mandate requires withdrawal approval it returns approvalRequired \u2014 same paste-approveUrl-then-retry flow as deposits.`;
1119
- async function runMcpPaymentServer(config) {
1120
- const { payer: payer2, signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1573
+ function createMcpPaymentServer(config) {
1574
+ const { signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1121
1575
  const vaultFlows = config.vaultFlows ?? null;
1122
1576
  const server = new Server(
1123
1577
  { name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
@@ -1132,7 +1586,7 @@ async function runMcpPaymentServer(config) {
1132
1586
  properties: {
1133
1587
  initialDepositRawUsdc: {
1134
1588
  type: "string",
1135
- description: `First deposit bundled into the owner's single Face ID (raw USDC, 6 decimals; just over 1 USDC minimum, e.g. "1010000"). Strongly recommended: without it the first deposit needs a separate approval.`
1589
+ description: `First deposit bundled into the owner's single Face ID (raw USDC, 6 decimals; the minimum depends on the vault, e.g. "1010000"). Strongly recommended: without it the first deposit needs a separate approval.`
1136
1590
  },
1137
1591
  approvalThresholdRawUsdc: {
1138
1592
  type: "string",
@@ -1250,12 +1704,26 @@ async function runMcpPaymentServer(config) {
1250
1704
  ];
1251
1705
  server.setRequestHandler(ListToolsRequestSchema, () => ({
1252
1706
  tools: [
1707
+ ...config.vaultSelection === void 0 ? [] : [
1708
+ {
1709
+ name: "list_subly_vaults",
1710
+ description: "List locally configured USDC Kamino vaults and the currently selected vault. Names come from on-chain metadata and may differ from Kamino's website.",
1711
+ inputSchema: { type: "object", properties: {} },
1712
+ annotations: { readOnlyHint: true, destructiveHint: false }
1713
+ },
1714
+ {
1715
+ name: "select_subly_vault",
1716
+ description: "Choose a vault from the local catalog for subsequent setup, deposit, budget, withdrawal, and payment tools. Checks the relayer supports the same vault. Does not move existing funds. Use the vault the user chose; never switch automatically based on APY.",
1717
+ inputSchema: { type: "object", properties: { vaultAddress: { type: "string" } }, required: ["vaultAddress"] },
1718
+ annotations: { readOnlyHint: false, destructiveHint: false }
1719
+ }
1720
+ ],
1253
1721
  ...vaultTools,
1254
1722
  {
1255
1723
  name: TOOL_NAME,
1256
1724
  description: `Fetch a URL (GET or POST), automatically paying a standard x402 (HTTP 402) challenge from a seller that offers Solana USDC \`exact\` with \`extra.feePayer\` (Nansen, etc.) out of the agent wallet's Kamino vault yield. Subly realizes just enough yield to the agent's USDC ATA (sponsored) and pays the seller's challenge; the seller needs no Subly integration. Returns the response body and, when a payment was made, the payment details (amount, payee, realize tx). Challenges above maxAmountRawUsdc (default ${defaultMaxAmountRawUsdc2} raw = ${formatRawUsdcAmount(
1257
1725
  defaultMaxAmountRawUsdc2
1258
- )} USDC) are refused without paying. Payments are refused when the spendable yield budget cannot cover them \u2014 the principal is never spent. Use only for URLs you intend to purchase access to.`,
1726
+ )} USDC) are refused without paying. Payments are refused when the spendable yield budget cannot cover them \u2014 principal is excluded by the relayer policy. Use only for URLs you intend to purchase access to.`,
1259
1727
  inputSchema: {
1260
1728
  type: "object",
1261
1729
  properties: {
@@ -1353,6 +1821,22 @@ async function runMcpPaymentServer(config) {
1353
1821
  );
1354
1822
  };
1355
1823
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1824
+ if (config.vaultSelection && request.params.name === "list_subly_vaults") {
1825
+ return textResult(config.vaultSelection.list());
1826
+ }
1827
+ if (config.vaultSelection && request.params.name === "select_subly_vault") {
1828
+ const address3 = request.params.arguments?.vaultAddress;
1829
+ if (typeof address3 !== "string") return textResult({ message: "vaultAddress is required" }, true);
1830
+ try {
1831
+ return textResult(await config.vaultSelection.select(address3, relayerBaseUrl2));
1832
+ } catch (error) {
1833
+ return vaultFlowFailure(error);
1834
+ }
1835
+ }
1836
+ const session = config.vaultSelection?.current();
1837
+ const signer3 = session?.signer ?? config.signer;
1838
+ const vaultFlows2 = session?.vaultFlows ?? config.vaultFlows ?? null;
1839
+ const payer2 = session?.payer ?? config.payer;
1356
1840
  const vaultToolNames = [
1357
1841
  BUDGET_TOOL_NAME,
1358
1842
  DEPOSIT_TOOL_NAME,
@@ -1360,18 +1844,18 @@ async function runMcpPaymentServer(config) {
1360
1844
  SETUP_TOOL_NAME,
1361
1845
  SETUP_STATUS_TOOL_NAME
1362
1846
  ];
1363
- if (vaultFlows !== null && vaultToolNames.includes(request.params.name)) {
1847
+ if (vaultFlows2 !== null && vaultToolNames.includes(request.params.name)) {
1364
1848
  const needsChainSync = request.params.name !== SETUP_TOOL_NAME && request.params.name !== SETUP_STATUS_TOOL_NAME;
1365
1849
  if (needsChainSync) {
1366
1850
  try {
1367
- await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
1851
+ await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer3 });
1368
1852
  } catch {
1369
1853
  }
1370
1854
  }
1371
1855
  const args2 = request.params.arguments ?? {};
1372
1856
  if (request.params.name === BUDGET_TOOL_NAME) {
1373
1857
  try {
1374
- const budget = await vaultFlows.getBudget();
1858
+ const budget = await vaultFlows2.getBudget();
1375
1859
  return textResult({
1376
1860
  ...budget,
1377
1861
  principalUsdc: formatRawUsdcAmount(
@@ -1402,7 +1886,7 @@ async function runMcpPaymentServer(config) {
1402
1886
  policy[key] = value;
1403
1887
  }
1404
1888
  }
1405
- const created = await vaultFlows.createSetupSession({
1889
+ const created = await vaultFlows2.createSetupSession({
1406
1890
  ...Object.keys(policy).length === 0 ? {} : { policy },
1407
1891
  ...typeof args2.mandateTtlDays === "number" ? { mandateTtlDays: args2.mandateTtlDays } : {},
1408
1892
  ...typeof args2.initialDepositRawUsdc === "string" ? { initialDepositRawUsdc: args2.initialDepositRawUsdc } : {}
@@ -1424,7 +1908,7 @@ async function runMcpPaymentServer(config) {
1424
1908
  );
1425
1909
  }
1426
1910
  try {
1427
- return textResult(await vaultFlows.getSetupSession(sessionId));
1911
+ return textResult(await vaultFlows2.getSetupSession(sessionId));
1428
1912
  } catch (error) {
1429
1913
  return vaultFlowFailure(error);
1430
1914
  }
@@ -1442,7 +1926,7 @@ async function runMcpPaymentServer(config) {
1442
1926
  const flowApprovalId = typeof args2.approvalId === "string" && args2.approvalId.length > 0 ? args2.approvalId : void 0;
1443
1927
  try {
1444
1928
  if (request.params.name === DEPOSIT_TOOL_NAME) {
1445
- const outcome2 = await vaultFlows.deposit({
1929
+ const outcome2 = await vaultFlows2.deposit({
1446
1930
  amountRawUsdc,
1447
1931
  ...flowApprovalId === void 0 ? {} : { approvalId: flowApprovalId }
1448
1932
  });
@@ -1452,7 +1936,7 @@ async function runMcpPaymentServer(config) {
1452
1936
  )
1453
1937
  });
1454
1938
  }
1455
- const outcome = await vaultFlows.withdraw({
1939
+ const outcome = await vaultFlows2.withdraw({
1456
1940
  amountRawUsdc,
1457
1941
  ...flowApprovalId === void 0 ? {} : { approvalId: flowApprovalId }
1458
1942
  });
@@ -1585,6 +2069,11 @@ async function runMcpPaymentServer(config) {
1585
2069
  };
1586
2070
  }
1587
2071
  });
2072
+ return server;
2073
+ }
2074
+ async function runMcpPaymentServer(config) {
2075
+ const server = createMcpPaymentServer(config);
2076
+ const { signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1588
2077
  try {
1589
2078
  await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
1590
2079
  console.error("[subly-mcp] wallet registered and synced at the relayer");
@@ -1600,136 +2089,6 @@ async function runMcpPaymentServer(config) {
1600
2089
  );
1601
2090
  }
1602
2091
 
1603
- // ../../src/client/relayer-yield-realizer.ts
1604
- var REALIZE_OVERHEAD_RAW_USDC = 2500n;
1605
- var RelayerRealizeError = class extends Error {
1606
- constructor(code, message, detail = null) {
1607
- super(message);
1608
- this.code = code;
1609
- this.detail = detail;
1610
- this.name = "RelayerRealizeError";
1611
- }
1612
- code;
1613
- detail;
1614
- };
1615
- var RelayerYieldRealizer = class {
1616
- vaultFlows;
1617
- constructor(config) {
1618
- this.vaultFlows = new VaultFlowClient({
1619
- relayerBaseUrl: config.relayerBaseUrl,
1620
- signer: config.signer,
1621
- rpc: config.rpc,
1622
- ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
1623
- ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
1624
- });
1625
- }
1626
- async ensureUsdcAvailable(input) {
1627
- const shortfallRawUsdc = input.amountRawUsdc;
1628
- await this.assertSpendableYield(shortfallRawUsdc);
1629
- let outcome;
1630
- try {
1631
- outcome = await this.vaultFlows.withdraw({
1632
- amountRawUsdc: shortfallRawUsdc,
1633
- // The relayer refuses to prepare this withdrawal beyond the spendable
1634
- // yield — the principal-protection guard the client cannot bypass.
1635
- purpose: "yield_realize",
1636
- // Declares what is being paid so the relayer's spending-mandate layer
1637
- // can enforce caps/payee and keep the mandate → payment audit chain.
1638
- ...input.payment === void 0 ? {} : { payment: input.payment },
1639
- ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1640
- });
1641
- } catch (error) {
1642
- throw this.mapWithdrawError(error);
1643
- }
1644
- if (outcome.status !== "confirmed" || outcome.txSignature === null) {
1645
- throw new RelayerRealizeError(
1646
- "realize_not_confirmed",
1647
- `yield realize withdrawal did not confirm (status=${outcome.status})`,
1648
- outcome
1649
- );
1650
- }
1651
- return {
1652
- realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
1653
- txSignature: outcome.txSignature,
1654
- withdrawalId: outcome.withdrawalId
1655
- };
1656
- }
1657
- /**
1658
- * Best-effort report-back of the x402 payment tx this realize funded —
1659
- * closes the relayer's mandate → realize → payment audit chain. Callers
1660
- * must never let a failure here affect the payment result.
1661
- */
1662
- async reportPayment(input) {
1663
- await this.vaultFlows.reportPayment(input);
1664
- }
1665
- /**
1666
- * Refuses to realize more than the ledger's spendable yield (principal).
1667
- * getBudget syncs the relayer's ledger from chain first (best-effort), so a
1668
- * long-running client sees yield as it accrues instead of a frozen view.
1669
- */
1670
- async assertSpendableYield(shortfallRawUsdc) {
1671
- let spendable;
1672
- try {
1673
- const budget = await this.vaultFlows.getBudget();
1674
- spendable = BigInt(budget.spendableYieldRawUsdc);
1675
- } catch (error) {
1676
- throw new RelayerRealizeError(
1677
- "budget_unavailable",
1678
- "could not read the spendable-yield budget",
1679
- error
1680
- );
1681
- }
1682
- const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
1683
- if (spendable < requiredRawUsdc) {
1684
- throw new RelayerRealizeError(
1685
- "insufficient_yield",
1686
- `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
1687
- { spendableYieldRawUsdc: spendable.toString() }
1688
- );
1689
- }
1690
- }
1691
- mapWithdrawError(error) {
1692
- if (!(error instanceof VaultFlowClientError)) {
1693
- return new RelayerRealizeError(
1694
- "prepare_failed",
1695
- `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
1696
- error
1697
- );
1698
- }
1699
- const serverCode = error.code ?? errorCodeFrom(error.detail);
1700
- if (serverCode === "approval_required") {
1701
- return new RelayerRealizeError(
1702
- "approval_required",
1703
- "this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
1704
- error.errorDetails ?? error.detail
1705
- );
1706
- }
1707
- if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
1708
- return new RelayerRealizeError(
1709
- "insufficient_yield",
1710
- "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
1711
- error.detail
1712
- );
1713
- }
1714
- return new RelayerRealizeError(
1715
- error.step === "submit" ? "submit_failed" : "prepare_failed",
1716
- error.message,
1717
- error.detail
1718
- );
1719
- }
1720
- };
1721
- function errorCodeFrom(detail) {
1722
- if (typeof detail !== "string") {
1723
- return null;
1724
- }
1725
- try {
1726
- const parsed = JSON.parse(detail);
1727
- return typeof parsed.error?.code === "string" ? parsed.error.code : null;
1728
- } catch {
1729
- return null;
1730
- }
1731
- }
1732
-
1733
2092
  // ../../src/client/relayer-payer.ts
1734
2093
  function createRelayerX402Payer(config) {
1735
2094
  const realizer = new RelayerYieldRealizer({
@@ -1749,22 +2108,22 @@ function createRelayerX402Payer(config) {
1749
2108
  import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
1750
2109
 
1751
2110
  // ../../src/solana/keys.ts
1752
- import { readFileSync } from "node:fs";
1753
- import bs582 from "bs58";
2111
+ import { readFileSync as readFileSync2 } from "node:fs";
2112
+ import bs583 from "bs58";
1754
2113
  import {
1755
2114
  createKeyPairSignerFromBytes
1756
2115
  } from "@solana/kit";
1757
2116
  function loadSecretKeyBytes(params) {
1758
2117
  const { base58Secret, jsonFilePath, label } = params;
1759
2118
  if (base58Secret !== void 0 && base58Secret.length > 0) {
1760
- const bytes = bs582.decode(base58Secret);
2119
+ const bytes = bs583.decode(base58Secret);
1761
2120
  if (bytes.length !== 64) {
1762
2121
  throw new Error(`${label} base58 secret must decode to 64 bytes`);
1763
2122
  }
1764
2123
  return bytes;
1765
2124
  }
1766
2125
  if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1767
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
2126
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
1768
2127
  if (!Array.isArray(raw) || raw.length !== 64) {
1769
2128
  throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1770
2129
  }
@@ -1779,7 +2138,7 @@ import bs587 from "bs58";
1779
2138
  import nacl3 from "tweetnacl";
1780
2139
 
1781
2140
  // ../../src/solana/tx.ts
1782
- import bs583 from "bs58";
2141
+ import bs584 from "bs58";
1783
2142
  import {
1784
2143
  appendTransactionMessageInstructions,
1785
2144
  compileTransaction,
@@ -1826,11 +2185,11 @@ function signatureBase58ForSigner(transaction, signer2) {
1826
2185
  if (signature === null || signature === void 0) {
1827
2186
  return null;
1828
2187
  }
1829
- return bs583.encode(signature);
2188
+ return bs584.encode(signature);
1830
2189
  }
1831
2190
 
1832
2191
  // ../../src/client/remote-signer-transport.ts
1833
- import bs584 from "bs58";
2192
+ import bs585 from "bs58";
1834
2193
  import nacl2 from "tweetnacl";
1835
2194
  var RemoteSigningError = class extends Error {
1836
2195
  constructor(provider, message, detail = null) {
@@ -1845,7 +2204,7 @@ var RemoteSigningError = class extends Error {
1845
2204
  function ed25519PublicKeyBytes(provider, walletAddress) {
1846
2205
  let bytes;
1847
2206
  try {
1848
- bytes = bs584.decode(walletAddress);
2207
+ bytes = bs585.decode(walletAddress);
1849
2208
  } catch {
1850
2209
  throw new RemoteSigningError(
1851
2210
  provider,
@@ -1861,10 +2220,10 @@ function ed25519PublicKeyBytes(provider, walletAddress) {
1861
2220
  return bytes;
1862
2221
  }
1863
2222
  function verifiedEd25519Signature(params) {
1864
- const publicKey = ed25519PublicKeyBytes(params.provider, params.walletAddress);
2223
+ const publicKey2 = ed25519PublicKeyBytes(params.provider, params.walletAddress);
1865
2224
  const encoded = params.encodedSignature.trim();
1866
2225
  for (const candidate of decodeSignatureCandidates(encoded)) {
1867
- if (nacl2.sign.detached.verify(params.message, candidate, publicKey)) {
2226
+ if (nacl2.sign.detached.verify(params.message, candidate, publicKey2)) {
1868
2227
  return candidate;
1869
2228
  }
1870
2229
  }
@@ -1880,7 +2239,7 @@ function decodeSignatureCandidates(encoded) {
1880
2239
  candidates.push(Uint8Array.from(Buffer.from(hex, "hex")));
1881
2240
  }
1882
2241
  try {
1883
- const fromBase58 = bs584.decode(encoded);
2242
+ const fromBase58 = bs585.decode(encoded);
1884
2243
  if (fromBase58.length === 64) {
1885
2244
  candidates.push(fromBase58);
1886
2245
  }
@@ -1918,8 +2277,8 @@ async function requestVerifiedTransactionSignature(params) {
1918
2277
  `signed transaction is missing the signature for ${transport.walletAddress}`
1919
2278
  );
1920
2279
  }
1921
- const publicKey = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
1922
- if (!nacl2.sign.detached.verify(params.messageBytes, signature, publicKey)) {
2280
+ const publicKey2 = params.publicKey ?? ed25519PublicKeyBytes(transport.provider, transport.walletAddress);
2281
+ if (!nacl2.sign.detached.verify(params.messageBytes, signature, publicKey2)) {
1923
2282
  throw new RemoteSigningError(
1924
2283
  transport.provider,
1925
2284
  "returned signature does not verify over the requested transaction"
@@ -1941,7 +2300,7 @@ async function externallySignedAgentTransaction(params) {
1941
2300
  });
1942
2301
  return {
1943
2302
  serializedTransaction: attached.serializedBase64,
1944
- agentSignature: bs584.encode(signature)
2303
+ agentSignature: bs585.encode(signature)
1945
2304
  };
1946
2305
  }
1947
2306
  async function providerJsonRequest(params) {
@@ -1985,98 +2344,6 @@ function computeRequestBindingHash(fields) {
1985
2344
  });
1986
2345
  }
1987
2346
 
1988
- // ../../src/lib/associated-token-account.ts
1989
- import { createHash as createHash4 } from "node:crypto";
1990
- import bs585 from "bs58";
1991
- var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
1992
- var ED25519_P = (1n << 255n) - 19n;
1993
- var ED25519_D = mod(-121665n * modPow(121666n, ED25519_P - 2n, ED25519_P), ED25519_P);
1994
- function deriveAssociatedTokenAddress(params) {
1995
- const owner = decodePublicKey(params.owner, "owner");
1996
- const mint = decodePublicKey(params.mint ?? SUBLY_VAULT.usdcMint, "mint");
1997
- const tokenProgramId = decodePublicKey(
1998
- params.tokenProgramId ?? SPL_TOKEN_PROGRAM_ID,
1999
- "tokenProgramId"
2000
- );
2001
- const associatedTokenProgramId = decodePublicKey(
2002
- ASSOCIATED_TOKEN_PROGRAM_ID,
2003
- "associatedTokenProgramId"
2004
- );
2005
- for (let bump = 255; bump >= 0; bump -= 1) {
2006
- const address3 = createProgramAddress(
2007
- [owner, tokenProgramId, mint, Uint8Array.of(bump)],
2008
- associatedTokenProgramId
2009
- );
2010
- if (address3 !== null) {
2011
- return bs585.encode(address3);
2012
- }
2013
- }
2014
- throw new Error("Unable to derive associated token account address");
2015
- }
2016
- function createProgramAddress(seeds, programId) {
2017
- const hash = createHash4("sha256");
2018
- for (const seed of seeds) {
2019
- hash.update(seed);
2020
- }
2021
- hash.update(programId);
2022
- hash.update(PDA_MARKER);
2023
- const digest = hash.digest();
2024
- return isEd25519Point(digest) ? null : new Uint8Array(digest);
2025
- }
2026
- function decodePublicKey(value, fieldName) {
2027
- const decoded = bs585.decode(value);
2028
- if (decoded.length !== 32) {
2029
- throw new Error(`${fieldName} must be a 32-byte public key`);
2030
- }
2031
- return decoded;
2032
- }
2033
- function isEd25519Point(bytes) {
2034
- if (bytes.length !== 32) {
2035
- return false;
2036
- }
2037
- const yBytes = Uint8Array.from(bytes);
2038
- yBytes[31] = yBytes[31] & 127;
2039
- const y = littleEndianToBigInt(yBytes);
2040
- if (y >= ED25519_P) {
2041
- return false;
2042
- }
2043
- const ySquared = mod(y * y, ED25519_P);
2044
- const numerator = mod(ySquared - 1n, ED25519_P);
2045
- const denominator = mod(ED25519_D * ySquared + 1n, ED25519_P);
2046
- if (denominator === 0n) {
2047
- return false;
2048
- }
2049
- const xSquared = mod(
2050
- numerator * modPow(denominator, ED25519_P - 2n, ED25519_P),
2051
- ED25519_P
2052
- );
2053
- return xSquared === 0n || modPow(xSquared, (ED25519_P - 1n) / 2n, ED25519_P) === 1n;
2054
- }
2055
- function littleEndianToBigInt(bytes) {
2056
- let value = 0n;
2057
- for (let index = bytes.length - 1; index >= 0; index -= 1) {
2058
- value = (value << 8n) + BigInt(bytes[index]);
2059
- }
2060
- return value;
2061
- }
2062
- function mod(value, modulus) {
2063
- const result = value % modulus;
2064
- return result >= 0n ? result : result + modulus;
2065
- }
2066
- function modPow(base, exponent, modulus) {
2067
- let result = 1n;
2068
- let nextBase = mod(base, modulus);
2069
- let nextExponent = exponent;
2070
- while (nextExponent > 0n) {
2071
- if ((nextExponent & 1n) === 1n) {
2072
- result = mod(result * nextBase, modulus);
2073
- }
2074
- nextBase = mod(nextBase * nextBase, modulus);
2075
- nextExponent >>= 1n;
2076
- }
2077
- return result;
2078
- }
2079
-
2080
2347
  // ../../src/client/transaction-intent-validator.ts
2081
2348
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
2082
2349
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
@@ -2219,16 +2486,16 @@ function validatePaymentIntentTransaction(params) {
2219
2486
  if (intent.network !== SOLANA_MAINNET_NETWORK) {
2220
2487
  reject("network_mismatch", "Unsupported network");
2221
2488
  }
2222
- if (intent.vault !== SUBLY_VAULT.address) {
2489
+ if (intent.vault !== policy.vault.address) {
2223
2490
  reject("vault_mismatch", "Unsupported vault");
2224
2491
  }
2225
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
2492
+ if (intent.shareMint !== policy.vault.shareMint) {
2226
2493
  reject("share_mint_mismatch", "Unsupported share mint");
2227
2494
  }
2228
- if (intent.farm !== SUBLY_VAULT.farm) {
2495
+ if (intent.farm !== policy.vault.farm) {
2229
2496
  reject("farm_mismatch", "Unsupported Kamino farm");
2230
2497
  }
2231
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
2498
+ if (intent.asset !== policy.vault.usdcMint) {
2232
2499
  reject("asset_mismatch", "Only USDC payments are supported");
2233
2500
  }
2234
2501
  if (intent.memo !== intent.paymentId) {
@@ -2344,7 +2611,7 @@ function validateDepositIntentTransaction(params) {
2344
2611
  if (new Date(intent.expiresAt).getTime() <= now) {
2345
2612
  reject("expired", "Deposit intent has expired");
2346
2613
  }
2347
- assertVaultIntentTargets(intent);
2614
+ assertVaultIntentTargets(intent, policy.vault);
2348
2615
  const decoded = decodeIntentTransaction({
2349
2616
  serializedTransaction: params.serializedTransaction,
2350
2617
  ...params.lookupTables === void 0 ? {} : { lookupTables: params.lookupTables }
@@ -2367,6 +2634,9 @@ function validateDepositIntentTransaction(params) {
2367
2634
  case MEMO_PROGRAM_ID:
2368
2635
  break;
2369
2636
  case KVAULT_PROGRAM_ID: {
2637
+ if (sawDeposit) {
2638
+ reject("duplicate_deposit", "A deposit intent authorizes exactly one KVault deposit");
2639
+ }
2370
2640
  if (!bytesStartWith(ix.data, KVAULT_DEPOSIT_DISCRIMINATOR)) {
2371
2641
  reject("unexpected_instruction", "Unexpected KVault instruction in deposit");
2372
2642
  }
@@ -2417,7 +2687,7 @@ function validateWithdrawalIntentTransaction(params) {
2417
2687
  if (new Date(intent.expiresAt).getTime() <= now) {
2418
2688
  reject("expired", "Withdrawal intent has expired");
2419
2689
  }
2420
- assertVaultIntentTargets(intent);
2690
+ assertVaultIntentTargets(intent, policy.vault);
2421
2691
  const expectedDestination = deriveAssociatedTokenAddress({
2422
2692
  owner: intent.wallet,
2423
2693
  mint: intent.asset
@@ -2515,22 +2785,23 @@ function validateWithdrawalIntentTransaction(params) {
2515
2785
  );
2516
2786
  }
2517
2787
  }
2518
- function assertVaultIntentTargets(intent) {
2519
- if (intent.vault !== SUBLY_VAULT.address) {
2788
+ function assertVaultIntentTargets(intent, vault) {
2789
+ if (intent.vault !== vault.address) {
2520
2790
  reject("vault_mismatch", "Unsupported vault");
2521
2791
  }
2522
- if (intent.shareMint !== SUBLY_VAULT.shareMint) {
2792
+ if (intent.shareMint !== vault.shareMint) {
2523
2793
  reject("share_mint_mismatch", "Unsupported share mint");
2524
2794
  }
2525
- if (intent.farm !== SUBLY_VAULT.farm) {
2795
+ if (intent.farm !== vault.farm) {
2526
2796
  reject("farm_mismatch", "Unsupported Kamino farm");
2527
2797
  }
2528
- if (intent.asset !== SUBLY_VAULT.usdcMint) {
2798
+ if (intent.asset !== vault.usdcMint) {
2529
2799
  reject("asset_mismatch", "Only USDC is supported");
2530
2800
  }
2531
2801
  }
2532
2802
  function resolveIntentValidationPolicy(policy) {
2533
2803
  const resolved = {
2804
+ vault: policy?.vault ?? SUBLY_VAULT,
2534
2805
  maxComputeUnitLimit: policy?.maxComputeUnitLimit ?? DEFAULT_MAX_COMPUTE_UNIT_LIMIT,
2535
2806
  maxComputeUnitPriceMicroLamports: policy?.maxComputeUnitPriceMicroLamports ?? DEFAULT_MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS,
2536
2807
  maxTemporaryAccountLamports: policy?.maxTemporaryAccountLamports ?? MAX_TEMP_ACCOUNT_LAMPORTS
@@ -2882,10 +3153,12 @@ function readShortVec(bytes, startOffset) {
2882
3153
 
2883
3154
  // ../../src/client/agent-wallet-signer.ts
2884
3155
  var IntentValidatingAgentWalletSigner = class {
3156
+ vault;
2885
3157
  validationMode = "structured_intent_transaction";
2886
3158
  validationPolicy;
2887
3159
  constructor(validationPolicy) {
2888
- this.validationPolicy = validationPolicy;
3160
+ this.vault = Object.freeze({ ...validationPolicy?.vault ?? SUBLY_VAULT });
3161
+ this.validationPolicy = { ...validationPolicy, vault: this.vault };
2889
3162
  }
2890
3163
  async signPayment(params) {
2891
3164
  this.assertIntentWallet(params.intent.wallet);
@@ -3043,15 +3316,15 @@ async function createCircleSignerTransport(config) {
3043
3316
  const entitySecretCiphertext = async () => {
3044
3317
  if (entityPublicKey === null) {
3045
3318
  const data = await request("GET", "/v1/w3s/config/entity/publicKey");
3046
- const publicKey = data.publicKey;
3047
- if (typeof publicKey !== "string") {
3319
+ const publicKey2 = data.publicKey;
3320
+ if (typeof publicKey2 !== "string") {
3048
3321
  throw new RemoteSigningError(
3049
3322
  PROVIDER,
3050
3323
  "entity public key response has no publicKey",
3051
3324
  data
3052
3325
  );
3053
3326
  }
3054
- entityPublicKey = createPublicKey(publicKey);
3327
+ entityPublicKey = createPublicKey(publicKey2);
3055
3328
  }
3056
3329
  return publicEncrypt(
3057
3330
  {
@@ -3322,16 +3595,40 @@ async function agentWalletSignerFromEnv(env = process.env) {
3322
3595
  `unknown SUBLY_SIGNER_PROVIDER "${provider}" (expected local, circle, or privy)`
3323
3596
  );
3324
3597
  }
3598
+ async function signerBundleForVault(bundle2, vault) {
3599
+ const signer2 = bundle2.provider === "local" ? new LocalKeypairAgentWalletSigner(await createKeyPairSignerFromBytes2(bundle2.localSecretKey), { vault }) : new RemoteAgentWalletSigner(bundle2.transport, { vault });
3600
+ return { ...bundle2, signer: signer2 };
3601
+ }
3325
3602
 
3326
3603
  // ../../src/client/standard-x402-state-store.ts
3327
- import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
3604
+ import { closeSync, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "node:fs";
3328
3605
  import { basename, dirname, join } from "node:path";
3329
3606
  function fileStandardX402StateStore(path) {
3330
3607
  return {
3608
+ async withExclusiveLock(operation) {
3609
+ mkdirSync(dirname(path), { recursive: true, mode: 448 });
3610
+ const lockPath = `${path}.lock`;
3611
+ let fd;
3612
+ try {
3613
+ fd = openSync(lockPath, "wx", 384);
3614
+ } catch (error) {
3615
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
3616
+ throw new Error(`Payment state is locked: ${lockPath}. Another client may be paying. After a crash, stop all clients before removing only this .lock file; preserve the payment state JSON.`);
3617
+ }
3618
+ throw error;
3619
+ }
3620
+ try {
3621
+ writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
3622
+ return await operation();
3623
+ } finally {
3624
+ closeSync(fd);
3625
+ unlinkSync(lockPath);
3626
+ }
3627
+ },
3331
3628
  load() {
3332
3629
  let text;
3333
3630
  try {
3334
- text = readFileSync2(path, "utf8");
3631
+ text = readFileSync3(path, "utf8");
3335
3632
  } catch (error) {
3336
3633
  if (isMissingFileError(error)) {
3337
3634
  return [];
@@ -3353,12 +3650,12 @@ function fileStandardX402StateStore(path) {
3353
3650
  },
3354
3651
  save(records) {
3355
3652
  const directory = dirname(path);
3356
- mkdirSync(directory, { recursive: true });
3653
+ mkdirSync(directory, { recursive: true, mode: 448 });
3357
3654
  const tempPath = join(
3358
3655
  directory,
3359
3656
  `.${basename(path)}.${process.pid}.${Date.now()}.tmp`
3360
3657
  );
3361
- writeFileSync(tempPath, JSON.stringify(records, null, 2));
3658
+ writeFileSync(tempPath, JSON.stringify(records, null, 2), { mode: 384, flag: "wx" });
3362
3659
  renameSync(tempPath, path);
3363
3660
  }
3364
3661
  };
@@ -3394,7 +3691,7 @@ async function svmTransactionSignerFromBundle(bundle2) {
3394
3691
  }
3395
3692
  function remoteSvmTransactionSigner(transport) {
3396
3693
  const signerAddress = address2(transport.walletAddress);
3397
- const publicKey = ed25519PublicKeyBytes(
3694
+ const publicKey2 = ed25519PublicKeyBytes(
3398
3695
  transport.provider,
3399
3696
  transport.walletAddress
3400
3697
  );
@@ -3407,7 +3704,7 @@ function remoteSvmTransactionSigner(transport) {
3407
3704
  transport,
3408
3705
  serializedTransactionBase64: getBase64EncodedWireTransaction2(transaction),
3409
3706
  messageBytes: transaction.messageBytes,
3410
- publicKey
3707
+ publicKey: publicKey2
3411
3708
  });
3412
3709
  dictionaries.push(
3413
3710
  Object.freeze({ [signerAddress]: signature })
@@ -3467,14 +3764,27 @@ var payer = createRelayerX402Payer({
3467
3764
  defaultMaxAmountRawUsdc,
3468
3765
  stateStore: fileStandardX402StateStore(pendingStatePath)
3469
3766
  });
3767
+ var catalog = vaultCatalogFromEnv();
3768
+ var sessions = [];
3769
+ for (const vault of catalog.vaults) {
3770
+ const selectedBundle = await signerBundleForVault(bundle, vault);
3771
+ const vaultSigner = selectedBundle.signer;
3772
+ const realizer = new RelayerYieldRealizer({ relayerBaseUrl, signer: vaultSigner, rpc });
3773
+ sessions.push({
3774
+ vault,
3775
+ signer: vaultSigner,
3776
+ // One payer owns deduplication and pending state across ALL vault selections.
3777
+ payer: { pay: (input) => payer.pay(input, realizer) },
3778
+ vaultFlows: new VaultFlowClient({ relayerBaseUrl, signer: vaultSigner, rpc })
3779
+ });
3780
+ }
3781
+ var vaultSelection = new McpVaultSelection(sessions, catalog.defaultVault);
3782
+ var selected = vaultSelection.current();
3470
3783
  await runMcpPaymentServer({
3471
- payer,
3472
- signer,
3784
+ payer: selected.payer,
3785
+ signer: selected.signer,
3473
3786
  relayerBaseUrl,
3474
3787
  defaultMaxAmountRawUsdc,
3475
- vaultFlows: new VaultFlowClient({
3476
- relayerBaseUrl,
3477
- signer,
3478
- rpc
3479
- })
3788
+ vaultFlows: selected.vaultFlows,
3789
+ vaultSelection
3480
3790
  });