@subly_fi/pay 0.2.0 → 0.4.0

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,16 +1,12 @@
1
- // demo/mcp-server.ts
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import {
5
- CallToolRequestSchema,
6
- ListToolsRequestSchema
7
- } from "@modelcontextprotocol/sdk/types.js";
1
+ // src/mcp-server.ts
2
+ import { homedir } from "node:os";
3
+ import { join as join2 } from "node:path";
8
4
 
9
- // src/client/agent-wallet-signer.ts
5
+ // ../../src/client/agent-wallet-signer.ts
10
6
  import { signBytes } from "@solana/kit";
11
7
  import bs584 from "bs58";
12
8
 
13
- // src/solana/tx.ts
9
+ // ../../src/solana/tx.ts
14
10
  import bs58 from "bs58";
15
11
  import {
16
12
  appendTransactionMessageInstructions,
@@ -25,8 +21,9 @@ import {
25
21
  setTransactionMessageLifetimeUsingBlockhash
26
22
  } from "@solana/kit";
27
23
 
28
- // src/lib/hash.ts
24
+ // ../../src/lib/hash.ts
29
25
  import { createHash } from "node:crypto";
26
+ var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
30
27
  function sha256TaggedHex(data) {
31
28
  return `sha256-${createHash("sha256").update(data).digest("hex")}`;
32
29
  }
@@ -50,7 +47,7 @@ function hashStableJson(value) {
50
47
  return sha256TaggedHex(stableStringify(value));
51
48
  }
52
49
 
53
- // src/solana/tx.ts
50
+ // ../../src/solana/tx.ts
54
51
  function decodeSerializedTransaction(serializedBase64) {
55
52
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
56
53
  }
@@ -70,11 +67,11 @@ function signatureBase58ForSigner(transaction, signer2) {
70
67
  return bs58.encode(signature);
71
68
  }
72
69
 
73
- // src/client/transaction-intent-validator.ts
70
+ // ../../src/client/transaction-intent-validator.ts
74
71
  import bs583 from "bs58";
75
72
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
76
73
 
77
- // src/config/constants.ts
74
+ // ../../src/config/constants.ts
78
75
  var PAYMENT_SCHEME = "subly-yield-exact";
79
76
  var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
80
77
  var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
@@ -90,7 +87,7 @@ var SUBLY_VAULT = {
90
87
  };
91
88
  var USDC_DECIMALS = 6;
92
89
 
93
- // src/domain/request-binding.ts
90
+ // ../../src/domain/request-binding.ts
94
91
  function computeRequestBindingHash(fields) {
95
92
  return hashStableJson({
96
93
  sellerRequestId: fields.sellerRequestId,
@@ -105,7 +102,7 @@ function computeRequestBindingHash(fields) {
105
102
  });
106
103
  }
107
104
 
108
- // src/lib/associated-token-account.ts
105
+ // ../../src/lib/associated-token-account.ts
109
106
  import { createHash as createHash2 } from "node:crypto";
110
107
  import bs582 from "bs58";
111
108
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
@@ -123,12 +120,12 @@ function deriveAssociatedTokenAddress(params) {
123
120
  "associatedTokenProgramId"
124
121
  );
125
122
  for (let bump = 255; bump >= 0; bump -= 1) {
126
- const address3 = createProgramAddress(
123
+ const address2 = createProgramAddress(
127
124
  [owner, tokenProgramId, mint, Uint8Array.of(bump)],
128
125
  associatedTokenProgramId
129
126
  );
130
- if (address3 !== null) {
131
- return bs582.encode(address3);
127
+ if (address2 !== null) {
128
+ return bs582.encode(address2);
132
129
  }
133
130
  }
134
131
  throw new Error("Unable to derive associated token account address");
@@ -197,7 +194,7 @@ function modPow(base, exponent, modulus) {
197
194
  return result;
198
195
  }
199
196
 
200
- // src/client/transaction-intent-validator.ts
197
+ // ../../src/client/transaction-intent-validator.ts
201
198
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
202
199
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
203
200
  var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
@@ -883,7 +880,7 @@ function readShortVec(bytes, startOffset) {
883
880
  return null;
884
881
  }
885
882
 
886
- // src/client/agent-wallet-signer.ts
883
+ // ../../src/client/agent-wallet-signer.ts
887
884
  var LocalKeypairAgentWalletSigner = class {
888
885
  validationMode = "structured_intent_transaction";
889
886
  keyPairSigner;
@@ -953,7 +950,15 @@ var LocalKeypairAgentWalletSigner = class {
953
950
  }
954
951
  };
955
952
 
956
- // src/api/wallet-auth.ts
953
+ // ../../src/client/mcp-payment-server.ts
954
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
955
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
956
+ import {
957
+ CallToolRequestSchema,
958
+ ListToolsRequestSchema
959
+ } from "@modelcontextprotocol/sdk/types.js";
960
+
961
+ // ../../src/api/wallet-auth.ts
957
962
  import { createHash as createHash3 } from "node:crypto";
958
963
  import bs585 from "bs58";
959
964
  import nacl from "tweetnacl";
@@ -971,7 +976,7 @@ function walletAuthMessage(params) {
971
976
  );
972
977
  }
973
978
 
974
- // src/client/wallet-auth-headers.ts
979
+ // ../../src/client/wallet-auth-headers.ts
975
980
  async function walletAuthHeaders(params) {
976
981
  const signedAtMs = String(Date.now());
977
982
  const message = walletAuthMessage({
@@ -987,7 +992,7 @@ async function walletAuthHeaders(params) {
987
992
  };
988
993
  }
989
994
 
990
- // src/client/onboarding.ts
995
+ // ../../src/client/onboarding.ts
991
996
  var SELF_SERVE_POLICY_ID = "self-serve";
992
997
  var OnboardingError = class extends Error {
993
998
  constructor(step, message, detail = null) {
@@ -1044,187 +1049,7 @@ async function ensureWalletOnboarded(params) {
1044
1049
  await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1045
1050
  }
1046
1051
 
1047
- // src/client/standard-x402-factory.ts
1048
- import { createX402Client } from "x402-solana/client";
1049
-
1050
- // src/client/relayer-yield-realizer.ts
1051
- import { address as address2 } from "@solana/kit";
1052
-
1053
- // src/client/lookup-tables.ts
1054
- import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1055
- import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
1056
- function lookupTableAddressesForTransaction(serializedTransaction) {
1057
- const wire = Buffer.from(serializedTransaction, "base64");
1058
- let offset = 0;
1059
- let signatureCount = 0;
1060
- let shift = 0;
1061
- while (offset < wire.length) {
1062
- const byte = wire[offset];
1063
- signatureCount |= (byte & 127) << shift;
1064
- offset += 1;
1065
- if ((byte & 128) === 0) {
1066
- break;
1067
- }
1068
- shift += 7;
1069
- }
1070
- const messageBytes = wire.subarray(offset + signatureCount * 64);
1071
- const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
1072
- const lookups = compiled.addressTableLookups ?? [];
1073
- return lookups.map((lookup) => String(lookup.lookupTableAddress));
1074
- }
1075
- async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1076
- const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1077
- if (addresses.length === 0) {
1078
- return {};
1079
- }
1080
- const tables = await fetchAllMaybeAddressLookupTable(
1081
- rpc2,
1082
- addresses.map((value) => address(value))
1083
- );
1084
- const result = {};
1085
- for (const table of tables) {
1086
- if (table.exists) {
1087
- result[table.address] = table.data.addresses.map(String);
1088
- }
1089
- }
1090
- return result;
1091
- }
1092
-
1093
- // src/client/relayer-yield-realizer.ts
1094
- var RelayerRealizeError = class extends Error {
1095
- constructor(code, message, detail = null) {
1096
- super(message);
1097
- this.code = code;
1098
- this.detail = detail;
1099
- this.name = "RelayerRealizeError";
1100
- }
1101
- code;
1102
- detail;
1103
- };
1104
- var RelayerYieldRealizer = class {
1105
- config;
1106
- signer;
1107
- rpc;
1108
- fetchImpl;
1109
- lookupTablesFor;
1110
- constructor(config) {
1111
- this.config = {
1112
- facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
1113
- usdcMint: config.usdcMint ?? SUBLY_VAULT.usdcMint
1114
- };
1115
- this.signer = config.signer;
1116
- this.rpc = config.rpc;
1117
- this.fetchImpl = config.fetchImpl ?? fetch;
1118
- this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
1119
- }
1120
- async ensureUsdcAvailable(input) {
1121
- const agentAta = deriveAssociatedTokenAddress({
1122
- owner: this.signer.walletAddress,
1123
- mint: this.config.usdcMint
1124
- });
1125
- const currentBalance = await this.readTokenBalance(agentAta);
1126
- if (currentBalance >= input.amountRawUsdc) {
1127
- return { realizedRawUsdc: 0n, txSignature: null };
1128
- }
1129
- const shortfallRawUsdc = input.amountRawUsdc - currentBalance;
1130
- await this.assertSpendableYield(shortfallRawUsdc);
1131
- const prepared = await this.postJson("/v1/withdrawals/prepare", {
1132
- wallet: this.signer.walletAddress,
1133
- amountRawUsdc: shortfallRawUsdc.toString()
1134
- });
1135
- const signed = await this.signer.signWithdrawal({
1136
- intent: prepared.signingIntent,
1137
- serializedTransaction: prepared.serializedTransaction,
1138
- lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1139
- });
1140
- const submitted = await this.postJson("/v1/withdrawals/submit", {
1141
- withdrawalId: prepared.withdrawalId,
1142
- serializedTransaction: signed.serializedTransaction,
1143
- agentSignature: signed.agentSignature
1144
- });
1145
- if (submitted.status !== "confirmed" || submitted.txSignature === null) {
1146
- throw new RelayerRealizeError(
1147
- "realize_not_confirmed",
1148
- `yield realize withdrawal did not confirm (status=${submitted.status})`,
1149
- submitted
1150
- );
1151
- }
1152
- return {
1153
- realizedRawUsdc: BigInt(submitted.actualWithdrawRawUsdc ?? "0"),
1154
- txSignature: submitted.txSignature
1155
- };
1156
- }
1157
- /** Refuses to realize more than the ledger's spendable yield (principal). */
1158
- async assertSpendableYield(shortfallRawUsdc) {
1159
- const url = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1160
- let response;
1161
- try {
1162
- response = await this.fetchImpl(url, {
1163
- headers: await walletAuthHeaders({
1164
- signer: this.signer,
1165
- method: "GET",
1166
- url
1167
- })
1168
- });
1169
- } catch (error) {
1170
- throw new RelayerRealizeError(
1171
- "budget_unavailable",
1172
- "could not read the spendable-yield budget",
1173
- error
1174
- );
1175
- }
1176
- if (response.status !== 200) {
1177
- throw new RelayerRealizeError(
1178
- "budget_unavailable",
1179
- `budget endpoint returned ${response.status}`
1180
- );
1181
- }
1182
- const body = await response.json();
1183
- const spendable = BigInt(body.budget?.spendableYieldRawUsdc ?? "0");
1184
- if (spendable < shortfallRawUsdc) {
1185
- throw new RelayerRealizeError(
1186
- "insufficient_yield",
1187
- `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
1188
- { spendableYieldRawUsdc: spendable.toString() }
1189
- );
1190
- }
1191
- }
1192
- async postJson(path, body) {
1193
- const url = `${this.config.facilitatorBaseUrl}${path}`;
1194
- const serialized = JSON.stringify(body);
1195
- const response = await this.fetchImpl(url, {
1196
- method: "POST",
1197
- headers: {
1198
- ...await walletAuthHeaders({
1199
- signer: this.signer,
1200
- method: "POST",
1201
- url,
1202
- body: serialized
1203
- }),
1204
- "content-type": "application/json"
1205
- },
1206
- body: serialized
1207
- });
1208
- const text = await response.text();
1209
- if (response.status !== 200) {
1210
- throw new RelayerRealizeError(
1211
- path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
1212
- `${path} failed with ${response.status}: ${text}`
1213
- );
1214
- }
1215
- return JSON.parse(text);
1216
- }
1217
- async readTokenBalance(ata) {
1218
- try {
1219
- const response = await this.rpc.getTokenAccountBalance(address2(ata), { commitment: "confirmed" }).send();
1220
- return BigInt(response.value.amount);
1221
- } catch {
1222
- return 0n;
1223
- }
1224
- }
1225
- };
1226
-
1227
- // src/x402/headers.ts
1052
+ // ../../src/x402/headers.ts
1228
1053
  import { z } from "zod";
1229
1054
  var PAYMENT_REQUIRED_HEADER = "payment-required";
1230
1055
  var MAX_HEADER_JSON_BYTES = 16384;
@@ -1290,8 +1115,26 @@ function decodeX402Header(headerValue) {
1290
1115
  );
1291
1116
  }
1292
1117
  }
1118
+ function requestBodyHashFor(body) {
1119
+ if (body === null || body === void 0 || body.length === 0) {
1120
+ return EMPTY_BODY_HASH;
1121
+ }
1122
+ return sha256TaggedHex(
1123
+ typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1124
+ );
1125
+ }
1126
+
1127
+ // ../../src/client/paid-fetch.ts
1128
+ function formatRawUsdcAmount(raw) {
1129
+ const value = BigInt(raw);
1130
+ const negative = value < 0n;
1131
+ const abs = negative ? -value : value;
1132
+ const whole = abs / 1000000n;
1133
+ const frac = (abs % 1000000n).toString().padStart(6, "0");
1134
+ return `${negative ? "-" : ""}${whole}.${frac}`;
1135
+ }
1293
1136
 
1294
- // src/x402/standard-requirements.ts
1137
+ // ../../src/x402/standard-requirements.ts
1295
1138
  import { z as z2 } from "zod";
1296
1139
  var STANDARD_EXACT_SCHEME = "exact";
1297
1140
  var standardExactRequirementSchema = z2.object({
@@ -1372,8 +1215,26 @@ function selectPayableSolanaRequirement(requirements, options) {
1372
1215
  feePayer: requirement.extra?.feePayer ?? null
1373
1216
  };
1374
1217
  }
1218
+ function standardRequirementMatchesSelected(candidate, selected) {
1219
+ const parsed = standardExactRequirementSchema.safeParse(candidate);
1220
+ return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
1221
+ }
1222
+ function stableJson(value) {
1223
+ return JSON.stringify(sortJson(value));
1224
+ }
1225
+ function sortJson(value) {
1226
+ if (Array.isArray(value)) {
1227
+ return value.map(sortJson);
1228
+ }
1229
+ if (value !== null && typeof value === "object") {
1230
+ return Object.fromEntries(
1231
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
1232
+ );
1233
+ }
1234
+ return value;
1235
+ }
1375
1236
 
1376
- // src/client/standard-x402-payer.ts
1237
+ // ../../src/client/standard-x402-payer.ts
1377
1238
  var StandardX402PayError = class extends Error {
1378
1239
  constructor(reason, message, detail = null) {
1379
1240
  super(message);
@@ -1391,6 +1252,10 @@ var StandardX402Payer = class {
1391
1252
  defaultMaxAmountRawUsdc;
1392
1253
  network;
1393
1254
  usdcMint;
1255
+ stateStore;
1256
+ pending = /* @__PURE__ */ new Map();
1257
+ inFlight = /* @__PURE__ */ new Map();
1258
+ nowMs;
1394
1259
  constructor(config) {
1395
1260
  this.realizer = config.realizer;
1396
1261
  this.x402Fetch = config.x402Fetch;
@@ -1398,10 +1263,59 @@ var StandardX402Payer = class {
1398
1263
  this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1399
1264
  this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1400
1265
  this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1266
+ this.stateStore = config.stateStore ?? null;
1267
+ this.nowMs = config.nowMs ?? (() => Date.now());
1268
+ if (this.stateStore !== null) {
1269
+ for (const record of this.stateStore.load()) {
1270
+ this.pending.set(record.key, record);
1271
+ }
1272
+ }
1273
+ }
1274
+ pay(input) {
1275
+ const method = (input.method ?? "GET").toUpperCase();
1276
+ const requestBodyHash = requestBodyHashFor(input.body ?? null);
1277
+ const pendingKey = pendingPaymentKey({
1278
+ url: input.url,
1279
+ method,
1280
+ requestBodyHash
1281
+ });
1282
+ const existingFlow = this.inFlight.get(pendingKey);
1283
+ if (existingFlow !== void 0) {
1284
+ return existingFlow;
1285
+ }
1286
+ const flow = this.run(input, {
1287
+ method,
1288
+ requestBodyHash,
1289
+ pendingKey
1290
+ }).finally(() => {
1291
+ this.inFlight.delete(pendingKey);
1292
+ });
1293
+ this.inFlight.set(pendingKey, flow);
1294
+ return flow;
1401
1295
  }
1402
- async pay(input) {
1296
+ async run(input, computed) {
1297
+ const { method, requestBodyHash, pendingKey } = computed;
1298
+ const existingPending = this.pending.get(pendingKey);
1299
+ if (existingPending !== void 0 && input.forceNewPayment !== true) {
1300
+ throw new StandardX402PayError(
1301
+ "payment_outcome_unknown",
1302
+ "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.",
1303
+ existingPending
1304
+ );
1305
+ }
1306
+ if (existingPending !== void 0 && input.forceNewPayment === true) {
1307
+ try {
1308
+ this.untrack(pendingKey);
1309
+ } catch (error) {
1310
+ throw new StandardX402PayError(
1311
+ "state_persist_failed",
1312
+ "could not clear the previous pending x402 marker before forcing a new payment",
1313
+ error
1314
+ );
1315
+ }
1316
+ }
1403
1317
  const init = {
1404
- method: input.method ?? "GET",
1318
+ method,
1405
1319
  ...input.body === void 0 ? {} : { body: input.body },
1406
1320
  ...input.headers === void 0 ? {} : { headers: input.headers }
1407
1321
  };
@@ -1430,15 +1344,55 @@ var StandardX402Payer = class {
1430
1344
  error
1431
1345
  );
1432
1346
  }
1433
- const response = await this.x402Fetch(input.url, init);
1347
+ const pendingRecord = {
1348
+ key: pendingKey,
1349
+ url: input.url,
1350
+ method,
1351
+ requestBodyHash,
1352
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1353
+ payTo: selected.payTo,
1354
+ feePayer: selected.feePayer,
1355
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1356
+ realizeTxSignature: realized.txSignature,
1357
+ status: "realized",
1358
+ createdAtMs: this.nowMs(),
1359
+ updatedAtMs: this.nowMs()
1360
+ };
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, selected);
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
+ }
1434
1383
  const bodyText = await response.text();
1435
1384
  if (response.status !== 200) {
1385
+ const persistError = this.tryMarkUnknown(pendingKey, {
1386
+ status: response.status,
1387
+ body: bodyText
1388
+ });
1436
1389
  throw new StandardX402PayError(
1437
- "delivery_failed",
1438
- `the x402 payment did not deliver (status ${response.status})`,
1439
- { status: response.status, body: bodyText }
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 }
1440
1393
  );
1441
1394
  }
1395
+ this.clearDelivered(pendingKey);
1442
1396
  return {
1443
1397
  paid: true,
1444
1398
  status: response.status,
@@ -1484,260 +1438,971 @@ var StandardX402Payer = class {
1484
1438
  );
1485
1439
  }
1486
1440
  }
1487
- };
1488
-
1489
- // src/client/web3-wallet-adapter.ts
1490
- import { Keypair } from "@solana/web3.js";
1491
- function createX402WalletAdapter(secretKey) {
1492
- if (secretKey.length !== 64) {
1493
- throw new Error("agent secret key must be 64 bytes");
1441
+ track(record) {
1442
+ const previous = this.pending.get(record.key);
1443
+ this.pending.set(record.key, record);
1444
+ try {
1445
+ this.persist();
1446
+ } catch (error) {
1447
+ if (previous === void 0) {
1448
+ this.pending.delete(record.key);
1449
+ } else {
1450
+ this.pending.set(record.key, previous);
1451
+ }
1452
+ throw error;
1453
+ }
1494
1454
  }
1495
- const keypair = Keypair.fromSecretKey(secretKey);
1496
- return {
1497
- address: keypair.publicKey.toBase58(),
1498
- signTransaction: async (tx) => {
1499
- tx.sign([keypair]);
1500
- return tx;
1455
+ markUnknown(key, detail) {
1456
+ const current = this.pending.get(key);
1457
+ if (current === void 0) {
1458
+ return;
1501
1459
  }
1502
- };
1503
- }
1504
-
1505
- // src/client/standard-x402-factory.ts
1506
- function createStandardX402Payer(config) {
1507
- const realizer = new RelayerYieldRealizer({
1508
- facilitatorBaseUrl: config.facilitatorBaseUrl,
1509
- signer: config.signer,
1510
- rpc: config.rpc
1511
- });
1512
- const client = createX402Client({
1513
- wallet: createX402WalletAdapter(config.agentSecretKey),
1514
- network: config.network ?? "solana",
1515
- rpcUrl: config.rpcUrl
1516
- });
1517
- const x402Fetch = (url, init) => client.fetch(url, init);
1518
- return new StandardX402Payer({
1519
- realizer,
1520
- x402Fetch,
1521
- defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
1522
- });
1523
- }
1524
-
1525
- // src/client/paid-fetch.ts
1526
- function formatRawUsdcAmount(raw) {
1527
- const value = BigInt(raw);
1528
- const negative = value < 0n;
1529
- const abs = negative ? -value : value;
1530
- const whole = abs / 1000000n;
1531
- const frac = (abs % 1000000n).toString().padStart(6, "0");
1532
- return `${negative ? "-" : ""}${whole}.${frac}`;
1533
- }
1534
-
1535
- // src/solana/keys.ts
1536
- import { readFileSync } from "node:fs";
1537
- import bs586 from "bs58";
1538
- import {
1539
- createKeyPairSignerFromBytes
1540
- } from "@solana/kit";
1541
- async function loadKeyPairSigner(params) {
1542
- const { base58Secret, jsonFilePath, label } = params;
1543
- if (base58Secret !== void 0 && base58Secret.length > 0) {
1544
- const bytes = bs586.decode(base58Secret);
1545
- if (bytes.length !== 64) {
1546
- throw new Error(`${label} base58 secret must decode to 64 bytes`);
1460
+ const next = {
1461
+ ...current,
1462
+ status: "external_outcome_unknown",
1463
+ updatedAtMs: this.nowMs(),
1464
+ detail
1465
+ };
1466
+ this.pending.set(key, next);
1467
+ try {
1468
+ this.persist();
1469
+ } catch (error) {
1470
+ this.pending.set(key, current);
1471
+ throw error;
1547
1472
  }
1548
- return createKeyPairSignerFromBytes(bytes);
1549
1473
  }
1550
- if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1551
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1552
- if (!Array.isArray(raw) || raw.length !== 64) {
1553
- throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1474
+ tryMarkUnknown(key, detail) {
1475
+ try {
1476
+ this.markUnknown(key, detail);
1477
+ return null;
1478
+ } catch (error) {
1479
+ return error;
1554
1480
  }
1555
- return createKeyPairSignerFromBytes(Uint8Array.from(raw));
1556
1481
  }
1557
- throw new Error(`${label} keypair is not configured`);
1558
- }
1559
- function loadSecretKeyBytes(params) {
1560
- const { base58Secret, jsonFilePath, label } = params;
1561
- if (base58Secret !== void 0 && base58Secret.length > 0) {
1562
- const bytes = bs586.decode(base58Secret);
1563
- if (bytes.length !== 64) {
1564
- throw new Error(`${label} base58 secret must decode to 64 bytes`);
1565
- }
1566
- return bytes;
1482
+ untrack(key) {
1483
+ const previous = this.pending.get(key);
1484
+ const existed = previous !== void 0;
1485
+ this.pending.delete(key);
1486
+ try {
1487
+ this.persist();
1488
+ } catch (error) {
1489
+ if (existed) {
1490
+ this.pending.set(key, previous);
1491
+ }
1492
+ throw error;
1493
+ }
1567
1494
  }
1568
- if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1569
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1570
- if (!Array.isArray(raw) || raw.length !== 64) {
1571
- throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1495
+ clearDelivered(key) {
1496
+ const previous = this.pending.get(key);
1497
+ this.pending.delete(key);
1498
+ try {
1499
+ this.persist();
1500
+ } catch (error) {
1501
+ if (previous !== void 0) {
1502
+ this.pending.set(key, previous);
1503
+ }
1504
+ console.error(
1505
+ `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
1506
+ );
1572
1507
  }
1573
- return Uint8Array.from(raw);
1574
1508
  }
1575
- throw new Error(`${label} keypair is not configured`);
1509
+ persist() {
1510
+ if (this.stateStore === null) {
1511
+ return;
1512
+ }
1513
+ this.stateStore.save([...this.pending.values()]);
1514
+ }
1515
+ };
1516
+ function pendingPaymentKey(input) {
1517
+ return `${input.method}:${input.url}:${input.requestBodyHash}`;
1576
1518
  }
1577
1519
 
1578
- // src/solana/rpc.ts
1579
- import { createSolanaRpc } from "@solana/kit";
1580
- function createRpc(url) {
1581
- return createSolanaRpc(url);
1520
+ // ../../src/client/lookup-tables.ts
1521
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1522
+ import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
1523
+ function lookupTableAddressesForTransaction(serializedTransaction) {
1524
+ const wire = Buffer.from(serializedTransaction, "base64");
1525
+ let offset = 0;
1526
+ let signatureCount = 0;
1527
+ let shift = 0;
1528
+ while (offset < wire.length) {
1529
+ const byte = wire[offset];
1530
+ signatureCount |= (byte & 127) << shift;
1531
+ offset += 1;
1532
+ if ((byte & 128) === 0) {
1533
+ break;
1534
+ }
1535
+ shift += 7;
1536
+ }
1537
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
1538
+ const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
1539
+ const lookups = compiled.addressTableLookups ?? [];
1540
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
1541
+ }
1542
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1543
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1544
+ if (addresses.length === 0) {
1545
+ return {};
1546
+ }
1547
+ const tables = await fetchAllMaybeAddressLookupTable(
1548
+ rpc2,
1549
+ addresses.map((value) => address(value))
1550
+ );
1551
+ const result = {};
1552
+ for (const table of tables) {
1553
+ if (table.exists) {
1554
+ result[table.address] = table.data.addresses.map(String);
1555
+ }
1556
+ }
1557
+ return result;
1582
1558
  }
1583
1559
 
1584
- // demo/mcp-server.ts
1560
+ // ../../src/client/vault-flows.ts
1561
+ var VaultFlowClientError = class extends Error {
1562
+ constructor(step, message, detail = null) {
1563
+ super(message);
1564
+ this.step = step;
1565
+ this.detail = detail;
1566
+ this.name = "VaultFlowClientError";
1567
+ }
1568
+ step;
1569
+ detail;
1570
+ };
1571
+ var VaultFlowClient = class {
1572
+ baseUrl;
1573
+ signer;
1574
+ fetchImpl;
1575
+ lookupTablesFor;
1576
+ pollTimeoutMs;
1577
+ pollIntervalMs;
1578
+ constructor(config) {
1579
+ this.baseUrl = config.facilitatorBaseUrl.replace(/\/$/, "");
1580
+ this.signer = config.signer;
1581
+ this.fetchImpl = config.fetchImpl ?? fetch;
1582
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1583
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1584
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1585
+ }
1586
+ /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1587
+ async deposit(input) {
1588
+ const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1589
+ wallet: this.signer.walletAddress,
1590
+ amountRawUsdc: input.amountRawUsdc.toString()
1591
+ });
1592
+ const signed = await this.signer.signDeposit({
1593
+ intent: prepared.signingIntent,
1594
+ serializedTransaction: prepared.serializedTransaction,
1595
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1596
+ });
1597
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
1598
+ depositId: prepared.depositId,
1599
+ serializedTransaction: signed.serializedTransaction,
1600
+ agentSignature: signed.agentSignature
1601
+ });
1602
+ if (outcome.status === "submitted") {
1603
+ outcome = await this.pollUntilTerminal(
1604
+ `/v1/deposits/${prepared.depositId}`,
1605
+ outcome
1606
+ );
1607
+ }
1608
+ return {
1609
+ depositId: prepared.depositId,
1610
+ status: outcome.status,
1611
+ txSignature: outcome.txSignature ?? null,
1612
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
1613
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
1614
+ errorCode: outcome.errorCode ?? null
1615
+ };
1616
+ }
1617
+ /**
1618
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
1619
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
1620
+ * with purpose "yield_realize" the relayer refuses anything beyond the
1621
+ * spendable yield (the payment path, via RelayerYieldRealizer).
1622
+ */
1623
+ async withdraw(input) {
1624
+ const prepared = await this.postJson(
1625
+ "prepare",
1626
+ "/v1/withdrawals/prepare",
1627
+ {
1628
+ wallet: this.signer.walletAddress,
1629
+ amountRawUsdc: input.amountRawUsdc.toString(),
1630
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1631
+ }
1632
+ );
1633
+ const signed = await this.signer.signWithdrawal({
1634
+ intent: prepared.signingIntent,
1635
+ serializedTransaction: prepared.serializedTransaction,
1636
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1637
+ });
1638
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
1639
+ withdrawalId: prepared.withdrawalId,
1640
+ serializedTransaction: signed.serializedTransaction,
1641
+ agentSignature: signed.agentSignature
1642
+ });
1643
+ if (outcome.status === "submitted") {
1644
+ outcome = await this.pollUntilTerminal(
1645
+ `/v1/withdrawals/${prepared.withdrawalId}`,
1646
+ outcome
1647
+ );
1648
+ }
1649
+ return {
1650
+ withdrawalId: prepared.withdrawalId,
1651
+ status: outcome.status,
1652
+ txSignature: outcome.txSignature ?? null,
1653
+ destinationUsdcAta: prepared.destinationUsdcAta,
1654
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
1655
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
1656
+ errorCode: outcome.errorCode ?? null
1657
+ };
1658
+ }
1659
+ /**
1660
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
1661
+ * yield accrued since the last sync shows up); the sync is best-effort and
1662
+ * on failure the last-synced view is returned.
1663
+ */
1664
+ async getBudget(options = {}) {
1665
+ if (options.refreshFromChain !== false) {
1666
+ try {
1667
+ await this.postJson(
1668
+ "sync",
1669
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
1670
+ { source: "chain" }
1671
+ );
1672
+ } catch {
1673
+ }
1674
+ }
1675
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1676
+ const response = await this.fetchImpl(url, {
1677
+ headers: await walletAuthHeaders({
1678
+ signer: this.signer,
1679
+ method: "GET",
1680
+ url
1681
+ })
1682
+ });
1683
+ const text = await response.text();
1684
+ if (response.status !== 200) {
1685
+ throw new VaultFlowClientError(
1686
+ "budget",
1687
+ `budget endpoint returned ${response.status}: ${text}`
1688
+ );
1689
+ }
1690
+ let parsed;
1691
+ try {
1692
+ parsed = JSON.parse(text);
1693
+ } catch {
1694
+ throw new VaultFlowClientError(
1695
+ "budget",
1696
+ "budget endpoint returned 200 with a non-JSON body",
1697
+ text
1698
+ );
1699
+ }
1700
+ const body = parsed;
1701
+ return {
1702
+ wallet: this.signer.walletAddress,
1703
+ principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1704
+ positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1705
+ grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
1706
+ spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1707
+ };
1708
+ }
1709
+ /**
1710
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
1711
+ * (each read looks the tx up on-chain) or the timeout elapses.
1712
+ */
1713
+ async pollUntilTerminal(path, last) {
1714
+ const deadline = Date.now() + this.pollTimeoutMs;
1715
+ let latest = last;
1716
+ while (Date.now() < deadline) {
1717
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1718
+ const url = `${this.baseUrl}${path}`;
1719
+ const response = await this.fetchImpl(url, {
1720
+ headers: await walletAuthHeaders({
1721
+ signer: this.signer,
1722
+ method: "GET",
1723
+ url
1724
+ })
1725
+ });
1726
+ if (response.status !== 200) {
1727
+ continue;
1728
+ }
1729
+ try {
1730
+ latest = await response.json();
1731
+ } catch {
1732
+ continue;
1733
+ }
1734
+ if (latest.status !== "submitted") {
1735
+ return latest;
1736
+ }
1737
+ }
1738
+ return latest;
1739
+ }
1740
+ async postJson(step, path, body) {
1741
+ const url = `${this.baseUrl}${path}`;
1742
+ const serialized = JSON.stringify(body);
1743
+ const response = await this.fetchImpl(url, {
1744
+ method: "POST",
1745
+ headers: {
1746
+ ...await walletAuthHeaders({
1747
+ signer: this.signer,
1748
+ method: "POST",
1749
+ url,
1750
+ body: serialized
1751
+ }),
1752
+ "content-type": "application/json"
1753
+ },
1754
+ body: serialized
1755
+ });
1756
+ const text = await response.text();
1757
+ if (response.status !== 200) {
1758
+ throw new VaultFlowClientError(
1759
+ step,
1760
+ `${path} failed with ${response.status}: ${text}`,
1761
+ text
1762
+ );
1763
+ }
1764
+ try {
1765
+ return JSON.parse(text);
1766
+ } catch {
1767
+ throw new VaultFlowClientError(
1768
+ step,
1769
+ `${path} returned 200 with a non-JSON body`,
1770
+ text
1771
+ );
1772
+ }
1773
+ }
1774
+ };
1775
+
1776
+ // ../../src/client/mcp-payment-server.ts
1585
1777
  var TOOL_NAME = "fetch_with_subly_payment";
1586
- var DEFAULT_MAX_AMOUNT_RAW_USDC = 10000n;
1587
- var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1588
- var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
1589
- var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? DEFAULT_MAX_AMOUNT_RAW_USDC : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
1590
- var keyPairSigner = await loadKeyPairSigner({
1591
- base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1592
- jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1593
- label: "SUBLY_DEMO_AGENT_KEYPAIR"
1594
- });
1595
- var agentSecretKey = loadSecretKeyBytes({
1596
- base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1597
- jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1598
- label: "SUBLY_DEMO_AGENT_KEYPAIR"
1599
- });
1600
- var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1601
- var rpc = createRpc(rpcUrl);
1602
- var payer = createStandardX402Payer({
1603
- facilitatorBaseUrl,
1604
- signer,
1605
- agentSecretKey,
1606
- rpc,
1607
- rpcUrl,
1608
- defaultMaxAmountRawUsdc
1609
- });
1778
+ var DEPOSIT_TOOL_NAME = "deposit_to_subly_vault";
1779
+ var WITHDRAW_TOOL_NAME = "withdraw_from_subly_vault";
1780
+ var BUDGET_TOOL_NAME = "get_subly_yield_budget";
1610
1781
  var SERVER_INSTRUCTIONS = `Subly lets an agent pay for ANY standard x402 (HTTP 402) paid API from its wallet's Kamino vault YIELD \u2014 the deposited principal is never spent, and the seller needs no Subly integration.
1611
1782
 
1612
- Before payments can succeed the operator of this server must have, once:
1613
- 1. A Solana keypair for the agent wallet. Subly does NOT create wallets; make one with \`solana-keygen new -o agent.json\` (or export a keypair from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it. The private key never leaves that file; this server only signs locally with it.
1614
- 2. Funded that wallet with USDC on Solana mainnet (no SOL needed \u2014 realize fees are sponsored) and deposited into the vault (see the project's deposit command). The vault minimum deposit is 1 USDC.
1615
- 3. Waited for yield to accrue; a payment needs the seller's price of spendable yield.
1783
+ One-time setup: the operator needs a Solana keypair for the agent wallet. Subly does NOT create wallets; make one with \`solana-keygen new -o agent.json\` (or export a keypair from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it. The private key never leaves that file; this server only signs locally with it. Then fund the wallet with USDC on Solana mainnet \u2014 no SOL is ever needed, all vault transaction fees are sponsored.
1616
1784
 
1617
- Then use fetch_with_subly_payment(url) to GET a paid resource from any x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's standard x402 challenge, returning the body plus the payment details. If it returns insufficient_yield, that is expected \u2014 wait for yield, do not loop.`;
1618
- var server = new Server(
1619
- { name: "subly-payments", version: "0.2.0" },
1620
- { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1621
- );
1622
- server.setRequestHandler(ListToolsRequestSchema, () => ({
1623
- tools: [
1785
+ From there the agent can do everything with these tools:
1786
+ 1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (minimum 1 USDC = 1000000 raw) so it starts earning yield.
1787
+ 2. get_subly_yield_budget() shows the principal, position value, and the spendable yield a payment can use right now.
1788
+ 3. fetch_with_subly_payment(url) GETs or POSTs a paid resource from any x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's standard x402 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.
1789
+ 4. withdraw_from_subly_vault(amountRawUsdc) exits: moves vault funds (principal included) back to the agent wallet's USDC account.`;
1790
+ async function runMcpPaymentServer(config) {
1791
+ const { payer: payer2, signer: signer2, facilitatorBaseUrl, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1792
+ const vaultFlows = config.vaultFlows ?? null;
1793
+ const server = new Server(
1794
+ { name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
1795
+ { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1796
+ );
1797
+ const vaultTools = vaultFlows === null ? [] : [
1624
1798
  {
1625
- name: TOOL_NAME,
1626
- description: `GET a URL, automatically paying a standard x402 (HTTP 402) challenge from any x402-compatible seller (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 Solana USDC \`exact\` 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 ${defaultMaxAmountRawUsdc} raw = ${formatRawUsdcAmount(
1627
- defaultMaxAmountRawUsdc
1628
- )} 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.`,
1799
+ name: DEPOSIT_TOOL_NAME,
1800
+ description: "Deposit USDC from the agent wallet into the Subly/Kamino vault so it starts earning the yield that funds x402 payments. The transaction fee is sponsored \u2014 the agent wallet needs USDC only, never SOL. The vault minimum deposit is 1 USDC (1000000 raw). The deposited amount becomes protected principal: payments can only ever spend the yield on top of it.",
1629
1801
  inputSchema: {
1630
1802
  type: "object",
1631
1803
  properties: {
1632
- url: {
1804
+ amountRawUsdc: {
1633
1805
  type: "string",
1634
- description: "URL to fetch (GET). Must match the seller's resource URL exactly."
1635
- },
1636
- maxAmountRawUsdc: {
1806
+ description: 'Amount to deposit in raw USDC units (6 decimals, e.g. "1000000" = 1 USDC). Must be at least 1000000.'
1807
+ }
1808
+ },
1809
+ required: ["amountRawUsdc"]
1810
+ },
1811
+ annotations: {
1812
+ title: "Deposit into the Subly vault",
1813
+ readOnlyHint: false,
1814
+ destructiveHint: false,
1815
+ idempotentHint: false,
1816
+ openWorldHint: false
1817
+ }
1818
+ },
1819
+ {
1820
+ name: WITHDRAW_TOOL_NAME,
1821
+ description: "Withdraw USDC from the Subly/Kamino vault back to the agent wallet's USDC account (fee sponsored, no SOL needed). This is the exit path and may spend PRINCIPAL \u2014 it reduces the deposit that earns yield. Limited to the vault's instant liquidity.",
1822
+ inputSchema: {
1823
+ type: "object",
1824
+ properties: {
1825
+ amountRawUsdc: {
1637
1826
  type: "string",
1638
- description: 'Refuse (without paying) any challenge above this amount in raw USDC units (6 decimals, e.g. "10000" = 0.01 USDC). Defaults to the server-side cap.'
1827
+ description: 'Amount to withdraw in raw USDC units (6 decimals, e.g. "1000000" = 1 USDC).'
1639
1828
  }
1640
1829
  },
1641
- required: ["url"]
1830
+ required: ["amountRawUsdc"]
1642
1831
  },
1643
1832
  annotations: {
1644
- title: "Fetch with Subly payment",
1833
+ title: "Withdraw from the Subly vault",
1645
1834
  readOnlyHint: false,
1646
1835
  destructiveHint: true,
1647
1836
  idempotentHint: false,
1648
- openWorldHint: true
1837
+ openWorldHint: false
1649
1838
  }
1839
+ },
1840
+ {
1841
+ name: BUDGET_TOOL_NAME,
1842
+ description: "Show the agent wallet's Subly vault budget: protected principal, current position value, and the spendable yield available for x402 payments right now. Syncs the position from chain first, so newly accrued yield is included.",
1843
+ inputSchema: { type: "object", properties: {} },
1844
+ annotations: {
1845
+ title: "Get Subly yield budget",
1846
+ readOnlyHint: true,
1847
+ destructiveHint: false,
1848
+ idempotentHint: true,
1849
+ openWorldHint: false
1850
+ }
1851
+ }
1852
+ ];
1853
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
1854
+ tools: [
1855
+ ...vaultTools,
1856
+ {
1857
+ name: TOOL_NAME,
1858
+ description: `Fetch a URL (GET or POST), automatically paying a standard x402 (HTTP 402) challenge from any x402-compatible seller (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 Solana USDC \`exact\` 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(
1859
+ defaultMaxAmountRawUsdc2
1860
+ )} 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.`,
1861
+ inputSchema: {
1862
+ type: "object",
1863
+ properties: {
1864
+ url: {
1865
+ type: "string",
1866
+ description: "URL to fetch. Must match the seller's resource URL exactly."
1867
+ },
1868
+ method: {
1869
+ type: "string",
1870
+ description: "HTTP method (default GET). Some x402 sellers deliver the paid resource over POST (e.g. an API that takes a JSON body)."
1871
+ },
1872
+ body: {
1873
+ type: "string",
1874
+ description: "Request body sent on both the probe and the paid retry. Provide a JSON string for POST sellers; sent as content-type application/json unless headers override it."
1875
+ },
1876
+ headers: {
1877
+ type: "object",
1878
+ description: "Extra request headers (object of string values), merged into both the probe and the paid retry.",
1879
+ additionalProperties: { type: "string" }
1880
+ },
1881
+ maxAmountRawUsdc: {
1882
+ type: "string",
1883
+ description: 'Refuse (without paying) any challenge above this amount in raw USDC units (6 decimals, e.g. "10000" = 0.01 USDC). Defaults to the server-side cap.'
1884
+ },
1885
+ forceNewPayment: {
1886
+ type: "boolean",
1887
+ description: "Pay again even if a previous external x402 attempt for the same URL/method/body has an unknown outcome. This may pay twice for the same resource."
1888
+ }
1889
+ },
1890
+ required: ["url"]
1891
+ },
1892
+ annotations: {
1893
+ title: "Fetch with Subly payment",
1894
+ readOnlyHint: false,
1895
+ destructiveHint: true,
1896
+ idempotentHint: false,
1897
+ openWorldHint: true
1898
+ }
1899
+ }
1900
+ ]
1901
+ }));
1902
+ const textResult = (value, isError = false) => ({
1903
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
1904
+ ...isError ? { isError: true } : {}
1905
+ });
1906
+ const vaultFlowFailure = (error) => {
1907
+ if (error instanceof VaultFlowClientError) {
1908
+ return textResult(
1909
+ { ok: false, step: error.step, message: error.message },
1910
+ true
1911
+ );
1650
1912
  }
1651
- ]
1652
- }));
1653
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1654
- if (request.params.name !== TOOL_NAME) {
1655
1913
  return {
1656
1914
  content: [
1657
- { type: "text", text: `unknown tool: ${request.params.name}` }
1915
+ {
1916
+ type: "text",
1917
+ text: error instanceof Error ? error.message : String(error)
1918
+ }
1658
1919
  ],
1659
1920
  isError: true
1660
1921
  };
1661
- }
1662
- const args = request.params.arguments ?? {};
1663
- const url = args.url;
1664
- if (typeof url !== "string" || url.length === 0) {
1665
- return {
1666
- content: [{ type: "text", text: "missing required argument: url" }],
1667
- isError: true
1668
- };
1669
- }
1670
- let maxAmountRawUsdc;
1671
- if (args.maxAmountRawUsdc !== void 0) {
1672
- const raw = args.maxAmountRawUsdc;
1922
+ };
1923
+ const parseRawAmount = (value) => {
1924
+ if (typeof value !== "string" && typeof value !== "number") {
1925
+ return null;
1926
+ }
1673
1927
  try {
1674
- if (typeof raw !== "string" && typeof raw !== "number") {
1675
- throw new TypeError("not a string or number");
1676
- }
1677
- maxAmountRawUsdc = BigInt(raw);
1928
+ const amount = BigInt(value);
1929
+ return amount > 0n ? amount : null;
1678
1930
  } catch {
1679
- return {
1680
- content: [
1931
+ return null;
1932
+ }
1933
+ };
1934
+ const vaultFlowOutcome = (outcome, amountField) => {
1935
+ const solscanUrl = outcome.txSignature === null ? null : `https://solscan.io/tx/${outcome.txSignature}`;
1936
+ if (outcome.status === "submitted") {
1937
+ return textResult(
1938
+ {
1939
+ ...outcome,
1940
+ ...amountField,
1941
+ solscanUrl,
1942
+ stillConfirming: true,
1943
+ warning: "the transaction was broadcast but had not confirmed before the poll timeout. Do NOT submit this deposit/withdrawal again \u2014 it may still confirm and moving the funds twice is not what the user asked for. Check get_subly_yield_budget in a minute, or the solscanUrl."
1944
+ },
1945
+ true
1946
+ );
1947
+ }
1948
+ return textResult(
1949
+ { ...outcome, ...amountField, solscanUrl },
1950
+ outcome.status !== "confirmed"
1951
+ );
1952
+ };
1953
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1954
+ const vaultToolNames = [
1955
+ BUDGET_TOOL_NAME,
1956
+ DEPOSIT_TOOL_NAME,
1957
+ WITHDRAW_TOOL_NAME
1958
+ ];
1959
+ if (vaultFlows !== null && vaultToolNames.includes(request.params.name)) {
1960
+ try {
1961
+ await ensureWalletOnboarded({ facilitatorBaseUrl, signer: signer2 });
1962
+ } catch {
1963
+ }
1964
+ if (request.params.name === BUDGET_TOOL_NAME) {
1965
+ try {
1966
+ const budget = await vaultFlows.getBudget();
1967
+ return textResult({
1968
+ ...budget,
1969
+ principalUsdc: formatRawUsdcAmount(
1970
+ BigInt(budget.principalBasisRawUsdc)
1971
+ ),
1972
+ positionValueUsdc: formatRawUsdcAmount(
1973
+ BigInt(budget.positionValueRawUsdc)
1974
+ ),
1975
+ spendableYieldUsdc: formatRawUsdcAmount(
1976
+ BigInt(budget.spendableYieldRawUsdc)
1977
+ )
1978
+ });
1979
+ } catch (error) {
1980
+ return vaultFlowFailure(error);
1981
+ }
1982
+ }
1983
+ const amountRawUsdc = parseRawAmount(
1984
+ (request.params.arguments ?? {}).amountRawUsdc
1985
+ );
1986
+ if (amountRawUsdc === null) {
1987
+ return textResult(
1681
1988
  {
1682
- type: "text",
1683
- text: "maxAmountRawUsdc must be an integer raw USDC amount"
1684
- }
1685
- ],
1989
+ ok: false,
1990
+ message: 'amountRawUsdc must be a positive integer raw USDC amount (6 decimals, e.g. "1000000" = 1 USDC)'
1991
+ },
1992
+ true
1993
+ );
1994
+ }
1995
+ try {
1996
+ if (request.params.name === DEPOSIT_TOOL_NAME) {
1997
+ const outcome2 = await vaultFlows.deposit({ amountRawUsdc });
1998
+ return vaultFlowOutcome(outcome2, {
1999
+ depositedUsdc: formatRawUsdcAmount(
2000
+ BigInt(outcome2.actualDepositRawUsdc ?? "0")
2001
+ )
2002
+ });
2003
+ }
2004
+ const outcome = await vaultFlows.withdraw({ amountRawUsdc });
2005
+ return vaultFlowOutcome(outcome, {
2006
+ withdrawnUsdc: formatRawUsdcAmount(
2007
+ BigInt(outcome.actualWithdrawRawUsdc ?? "0")
2008
+ )
2009
+ });
2010
+ } catch (error) {
2011
+ return vaultFlowFailure(error);
2012
+ }
2013
+ }
2014
+ if (request.params.name !== TOOL_NAME) {
2015
+ return {
2016
+ content: [{ type: "text", text: `unknown tool: ${request.params.name}` }],
1686
2017
  isError: true
1687
2018
  };
1688
2019
  }
1689
- }
1690
- try {
1691
- const result = await payer.pay({
1692
- url,
1693
- ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
1694
- });
1695
- return {
1696
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1697
- };
1698
- } catch (error) {
1699
- if (error instanceof StandardX402PayError) {
2020
+ const args = request.params.arguments ?? {};
2021
+ const url = args.url;
2022
+ if (typeof url !== "string" || url.length === 0) {
2023
+ return {
2024
+ content: [{ type: "text", text: "missing required argument: url" }],
2025
+ isError: true
2026
+ };
2027
+ }
2028
+ let maxAmountRawUsdc;
2029
+ if (args.maxAmountRawUsdc !== void 0) {
2030
+ const raw = args.maxAmountRawUsdc;
2031
+ try {
2032
+ if (typeof raw !== "string" && typeof raw !== "number") {
2033
+ throw new TypeError("not a string or number");
2034
+ }
2035
+ maxAmountRawUsdc = BigInt(raw);
2036
+ } catch {
2037
+ return {
2038
+ content: [
2039
+ {
2040
+ type: "text",
2041
+ text: "maxAmountRawUsdc must be an integer raw USDC amount"
2042
+ }
2043
+ ],
2044
+ isError: true
2045
+ };
2046
+ }
2047
+ }
2048
+ const method = typeof args.method === "string" ? args.method : void 0;
2049
+ const body = typeof args.body === "string" ? args.body : void 0;
2050
+ const forceNewPayment = args.forceNewPayment === true;
2051
+ const headers = args.headers !== null && typeof args.headers === "object" && !Array.isArray(args.headers) ? Object.fromEntries(
2052
+ Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
2053
+ ) : void 0;
2054
+ const mergedHeaders = body === void 0 ? headers : { "content-type": "application/json", ...headers ?? {} };
2055
+ try {
2056
+ const result = await payer2.pay({
2057
+ url,
2058
+ ...method === void 0 ? {} : { method },
2059
+ ...body === void 0 ? {} : { body },
2060
+ ...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
2061
+ ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
2062
+ ...forceNewPayment ? { forceNewPayment } : {}
2063
+ });
2064
+ return {
2065
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2066
+ };
2067
+ } catch (error) {
2068
+ if (error instanceof StandardX402PayError) {
2069
+ return {
2070
+ content: [
2071
+ {
2072
+ type: "text",
2073
+ text: JSON.stringify(
2074
+ {
2075
+ paid: false,
2076
+ refused: true,
2077
+ reason: error.reason,
2078
+ message: error.message,
2079
+ detail: error.detail
2080
+ },
2081
+ null,
2082
+ 2
2083
+ )
2084
+ }
2085
+ ],
2086
+ isError: true
2087
+ };
2088
+ }
1700
2089
  return {
1701
2090
  content: [
1702
2091
  {
1703
2092
  type: "text",
1704
- text: JSON.stringify(
1705
- {
1706
- paid: false,
1707
- refused: true,
1708
- reason: error.reason,
1709
- message: error.message,
1710
- detail: error.detail
1711
- },
1712
- null,
1713
- 2
1714
- )
2093
+ text: error instanceof Error ? error.message : String(error)
1715
2094
  }
1716
2095
  ],
1717
2096
  isError: true
1718
2097
  };
1719
2098
  }
2099
+ });
2100
+ try {
2101
+ await ensureWalletOnboarded({ facilitatorBaseUrl, signer: signer2 });
2102
+ console.error("[subly-mcp] wallet registered and synced at the relayer");
2103
+ } catch (error) {
2104
+ console.error(
2105
+ `[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
2106
+ );
2107
+ }
2108
+ const transport = new StdioServerTransport();
2109
+ await server.connect(transport);
2110
+ console.error(
2111
+ `[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
2112
+ );
2113
+ }
2114
+
2115
+ // ../../src/client/relayer-yield-realizer.ts
2116
+ var REALIZE_OVERHEAD_RAW_USDC = 2500n;
2117
+ var RelayerRealizeError = class extends Error {
2118
+ constructor(code, message, detail = null) {
2119
+ super(message);
2120
+ this.code = code;
2121
+ this.detail = detail;
2122
+ this.name = "RelayerRealizeError";
2123
+ }
2124
+ code;
2125
+ detail;
2126
+ };
2127
+ var RelayerYieldRealizer = class {
2128
+ vaultFlows;
2129
+ constructor(config) {
2130
+ this.vaultFlows = new VaultFlowClient({
2131
+ facilitatorBaseUrl: config.facilitatorBaseUrl,
2132
+ signer: config.signer,
2133
+ rpc: config.rpc,
2134
+ ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
2135
+ ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
2136
+ });
2137
+ }
2138
+ async ensureUsdcAvailable(input) {
2139
+ const shortfallRawUsdc = input.amountRawUsdc;
2140
+ await this.assertSpendableYield(shortfallRawUsdc);
2141
+ let outcome;
2142
+ try {
2143
+ outcome = await this.vaultFlows.withdraw({
2144
+ amountRawUsdc: shortfallRawUsdc,
2145
+ // The relayer refuses to prepare this withdrawal beyond the spendable
2146
+ // yield — the principal-protection guard the client cannot bypass.
2147
+ purpose: "yield_realize"
2148
+ });
2149
+ } catch (error) {
2150
+ throw this.mapWithdrawError(error);
2151
+ }
2152
+ if (outcome.status !== "confirmed" || outcome.txSignature === null) {
2153
+ throw new RelayerRealizeError(
2154
+ "realize_not_confirmed",
2155
+ `yield realize withdrawal did not confirm (status=${outcome.status})`,
2156
+ outcome
2157
+ );
2158
+ }
1720
2159
  return {
1721
- content: [
2160
+ realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
2161
+ txSignature: outcome.txSignature
2162
+ };
2163
+ }
2164
+ /**
2165
+ * Refuses to realize more than the ledger's spendable yield (principal).
2166
+ * getBudget syncs the relayer's ledger from chain first (best-effort), so a
2167
+ * long-running client sees yield as it accrues instead of a frozen view.
2168
+ */
2169
+ async assertSpendableYield(shortfallRawUsdc) {
2170
+ let spendable;
2171
+ try {
2172
+ const budget = await this.vaultFlows.getBudget();
2173
+ spendable = BigInt(budget.spendableYieldRawUsdc);
2174
+ } catch (error) {
2175
+ throw new RelayerRealizeError(
2176
+ "budget_unavailable",
2177
+ "could not read the spendable-yield budget",
2178
+ error
2179
+ );
2180
+ }
2181
+ const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
2182
+ if (spendable < requiredRawUsdc) {
2183
+ throw new RelayerRealizeError(
2184
+ "insufficient_yield",
2185
+ `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`,
2186
+ { spendableYieldRawUsdc: spendable.toString() }
2187
+ );
2188
+ }
2189
+ }
2190
+ mapWithdrawError(error) {
2191
+ if (!(error instanceof VaultFlowClientError)) {
2192
+ return new RelayerRealizeError(
2193
+ "prepare_failed",
2194
+ `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
2195
+ error
2196
+ );
2197
+ }
2198
+ const serverCode = errorCodeFrom(error.detail);
2199
+ if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
2200
+ return new RelayerRealizeError(
2201
+ "insufficient_yield",
2202
+ "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
2203
+ error.detail
2204
+ );
2205
+ }
2206
+ return new RelayerRealizeError(
2207
+ error.step === "submit" ? "submit_failed" : "prepare_failed",
2208
+ error.message,
2209
+ error.detail
2210
+ );
2211
+ }
2212
+ };
2213
+ function errorCodeFrom(detail) {
2214
+ if (typeof detail !== "string") {
2215
+ return null;
2216
+ }
2217
+ try {
2218
+ const parsed = JSON.parse(detail);
2219
+ return typeof parsed.error?.code === "string" ? parsed.error.code : null;
2220
+ } catch {
2221
+ return null;
2222
+ }
2223
+ }
2224
+
2225
+ // ../../src/client/relayer-payer.ts
2226
+ function createRelayerX402Payer(config) {
2227
+ const realizer = new RelayerYieldRealizer({
2228
+ facilitatorBaseUrl: config.facilitatorBaseUrl,
2229
+ signer: config.signer,
2230
+ rpc: config.rpc
2231
+ });
2232
+ return new StandardX402Payer({
2233
+ realizer,
2234
+ x402Fetch: config.x402Fetch,
2235
+ defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc,
2236
+ ...config.stateStore === void 0 ? {} : { stateStore: config.stateStore }
2237
+ });
2238
+ }
2239
+
2240
+ // ../../src/client/standard-x402-state-store.ts
2241
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2242
+ import { basename, dirname, join } from "node:path";
2243
+ function fileStandardX402StateStore(path) {
2244
+ return {
2245
+ load() {
2246
+ let text;
2247
+ try {
2248
+ text = readFileSync(path, "utf8");
2249
+ } catch (error) {
2250
+ if (isMissingFileError(error)) {
2251
+ return [];
2252
+ }
2253
+ throw error;
2254
+ }
2255
+ const parsed = JSON.parse(text);
2256
+ if (!Array.isArray(parsed)) {
2257
+ throw new Error(`pending payment state is not an array: ${path}`);
2258
+ }
2259
+ for (const [index, record] of parsed.entries()) {
2260
+ if (!isPendingPaymentRecord(record)) {
2261
+ throw new Error(
2262
+ `pending payment state has an invalid record at index ${index}: ${path}`
2263
+ );
2264
+ }
2265
+ }
2266
+ return parsed;
2267
+ },
2268
+ save(records) {
2269
+ const directory = dirname(path);
2270
+ mkdirSync(directory, { recursive: true });
2271
+ const tempPath = join(
2272
+ directory,
2273
+ `.${basename(path)}.${process.pid}.${Date.now()}.tmp`
2274
+ );
2275
+ writeFileSync(tempPath, JSON.stringify(records, null, 2));
2276
+ renameSync(tempPath, path);
2277
+ }
2278
+ };
2279
+ }
2280
+ function isMissingFileError(error) {
2281
+ return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
2282
+ }
2283
+ function isPendingPaymentRecord(value) {
2284
+ if (value === null || typeof value !== "object") {
2285
+ return false;
2286
+ }
2287
+ const record = value;
2288
+ return typeof record.key === "string" && typeof record.url === "string" && typeof record.method === "string" && typeof record.requestBodyHash === "string" && typeof record.amountRawUsdc === "string" && typeof record.payTo === "string" && (record.feePayer === null || typeof record.feePayer === "string") && typeof record.realizedRawUsdc === "string" && (record.realizeTxSignature === null || typeof record.realizeTxSignature === "string") && (record.status === "realized" || record.status === "external_outcome_unknown") && typeof record.createdAtMs === "number" && typeof record.updatedAtMs === "number";
2289
+ }
2290
+
2291
+ // ../../src/solana/keys.ts
2292
+ import { readFileSync as readFileSync2 } from "node:fs";
2293
+ import bs586 from "bs58";
2294
+ import {
2295
+ createKeyPairSignerFromBytes
2296
+ } from "@solana/kit";
2297
+ async function loadKeyPairSigner(params) {
2298
+ const { base58Secret, jsonFilePath, label } = params;
2299
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
2300
+ const bytes = bs586.decode(base58Secret);
2301
+ if (bytes.length !== 64) {
2302
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
2303
+ }
2304
+ return createKeyPairSignerFromBytes(bytes);
2305
+ }
2306
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
2307
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
2308
+ if (!Array.isArray(raw) || raw.length !== 64) {
2309
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
2310
+ }
2311
+ return createKeyPairSignerFromBytes(Uint8Array.from(raw));
2312
+ }
2313
+ throw new Error(`${label} keypair is not configured`);
2314
+ }
2315
+ function loadSecretKeyBytes(params) {
2316
+ const { base58Secret, jsonFilePath, label } = params;
2317
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
2318
+ const bytes = bs586.decode(base58Secret);
2319
+ if (bytes.length !== 64) {
2320
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
2321
+ }
2322
+ return bytes;
2323
+ }
2324
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
2325
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
2326
+ if (!Array.isArray(raw) || raw.length !== 64) {
2327
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
2328
+ }
2329
+ return Uint8Array.from(raw);
2330
+ }
2331
+ throw new Error(`${label} keypair is not configured`);
2332
+ }
2333
+
2334
+ // ../../src/solana/rpc.ts
2335
+ import { createSolanaRpc } from "@solana/kit";
2336
+ function createRpc(url) {
2337
+ return createSolanaRpc(url);
2338
+ }
2339
+
2340
+ // src/svm-x402-fetch.ts
2341
+ import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
2342
+ import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
2343
+ import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
2344
+ async function createSvmX402Fetch(params) {
2345
+ const signer2 = toClientSvmSigner(
2346
+ await createKeyPairSignerFromBytes2(params.agentSecretKey)
2347
+ );
2348
+ return (url, init, expected) => {
2349
+ const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
2350
+ schemes: [
1722
2351
  {
1723
- type: "text",
1724
- text: error instanceof Error ? error.message : String(error)
2352
+ network: expected.requirement.network,
2353
+ client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
1725
2354
  }
1726
2355
  ],
1727
- isError: true
1728
- };
1729
- }
1730
- });
1731
- try {
1732
- await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
1733
- console.error("[subly-mcp] wallet registered and synced at the relayer");
1734
- } catch (error) {
1735
- console.error(
1736
- `[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
2356
+ paymentRequirementsSelector: (_x402Version, requirements) => selectExpectedRequirement(requirements, expected)
2357
+ });
2358
+ return wrapped(url, init);
2359
+ };
2360
+ }
2361
+ function selectExpectedRequirement(requirements, expected) {
2362
+ const match = requirements.find(
2363
+ (candidate) => standardRequirementMatchesSelected(candidate, expected)
1737
2364
  );
2365
+ if (match === void 0) {
2366
+ throw new Error(
2367
+ "x402 challenge changed after preflight; refusing to pay an unchecked requirement"
2368
+ );
2369
+ }
2370
+ return match;
1738
2371
  }
1739
- var transport = new StdioServerTransport();
1740
- await server.connect(transport);
1741
- console.error(
1742
- `[subly-mcp] ready: agent wallet ${signer.walletAddress}, relayer ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc)} USDC`
1743
- );
2372
+
2373
+ // src/mcp-server.ts
2374
+ var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
2375
+ var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
2376
+ var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
2377
+ var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? join2(homedir(), ".subly", "standard-x402-pending.json");
2378
+ var keyPairSigner = await loadKeyPairSigner({
2379
+ base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
2380
+ jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
2381
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
2382
+ });
2383
+ var agentSecretKey = loadSecretKeyBytes({
2384
+ base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
2385
+ jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
2386
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
2387
+ });
2388
+ var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
2389
+ var rpc = createRpc(rpcUrl);
2390
+ var payer = createRelayerX402Payer({
2391
+ facilitatorBaseUrl: relayerBaseUrl,
2392
+ signer,
2393
+ rpc,
2394
+ x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
2395
+ defaultMaxAmountRawUsdc,
2396
+ stateStore: fileStandardX402StateStore(pendingStatePath)
2397
+ });
2398
+ await runMcpPaymentServer({
2399
+ payer,
2400
+ signer,
2401
+ facilitatorBaseUrl: relayerBaseUrl,
2402
+ defaultMaxAmountRawUsdc,
2403
+ vaultFlows: new VaultFlowClient({
2404
+ facilitatorBaseUrl: relayerBaseUrl,
2405
+ signer,
2406
+ rpc
2407
+ })
2408
+ });