@subly_fi/pay 0.2.0 → 0.3.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,8 @@
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";
8
-
9
- // src/client/agent-wallet-signer.ts
1
+ // ../../src/client/agent-wallet-signer.ts
10
2
  import { signBytes } from "@solana/kit";
11
3
  import bs584 from "bs58";
12
4
 
13
- // src/solana/tx.ts
5
+ // ../../src/solana/tx.ts
14
6
  import bs58 from "bs58";
15
7
  import {
16
8
  appendTransactionMessageInstructions,
@@ -25,7 +17,7 @@ import {
25
17
  setTransactionMessageLifetimeUsingBlockhash
26
18
  } from "@solana/kit";
27
19
 
28
- // src/lib/hash.ts
20
+ // ../../src/lib/hash.ts
29
21
  import { createHash } from "node:crypto";
30
22
  function sha256TaggedHex(data) {
31
23
  return `sha256-${createHash("sha256").update(data).digest("hex")}`;
@@ -50,7 +42,7 @@ function hashStableJson(value) {
50
42
  return sha256TaggedHex(stableStringify(value));
51
43
  }
52
44
 
53
- // src/solana/tx.ts
45
+ // ../../src/solana/tx.ts
54
46
  function decodeSerializedTransaction(serializedBase64) {
55
47
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
56
48
  }
@@ -70,11 +62,11 @@ function signatureBase58ForSigner(transaction, signer2) {
70
62
  return bs58.encode(signature);
71
63
  }
72
64
 
73
- // src/client/transaction-intent-validator.ts
65
+ // ../../src/client/transaction-intent-validator.ts
74
66
  import bs583 from "bs58";
75
67
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
76
68
 
77
- // src/config/constants.ts
69
+ // ../../src/config/constants.ts
78
70
  var PAYMENT_SCHEME = "subly-yield-exact";
79
71
  var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
80
72
  var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
@@ -90,7 +82,7 @@ var SUBLY_VAULT = {
90
82
  };
91
83
  var USDC_DECIMALS = 6;
92
84
 
93
- // src/domain/request-binding.ts
85
+ // ../../src/domain/request-binding.ts
94
86
  function computeRequestBindingHash(fields) {
95
87
  return hashStableJson({
96
88
  sellerRequestId: fields.sellerRequestId,
@@ -105,7 +97,7 @@ function computeRequestBindingHash(fields) {
105
97
  });
106
98
  }
107
99
 
108
- // src/lib/associated-token-account.ts
100
+ // ../../src/lib/associated-token-account.ts
109
101
  import { createHash as createHash2 } from "node:crypto";
110
102
  import bs582 from "bs58";
111
103
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
@@ -197,7 +189,7 @@ function modPow(base, exponent, modulus) {
197
189
  return result;
198
190
  }
199
191
 
200
- // src/client/transaction-intent-validator.ts
192
+ // ../../src/client/transaction-intent-validator.ts
201
193
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
202
194
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
203
195
  var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
@@ -883,7 +875,7 @@ function readShortVec(bytes, startOffset) {
883
875
  return null;
884
876
  }
885
877
 
886
- // src/client/agent-wallet-signer.ts
878
+ // ../../src/client/agent-wallet-signer.ts
887
879
  var LocalKeypairAgentWalletSigner = class {
888
880
  validationMode = "structured_intent_transaction";
889
881
  keyPairSigner;
@@ -953,7 +945,15 @@ var LocalKeypairAgentWalletSigner = class {
953
945
  }
954
946
  };
955
947
 
956
- // src/api/wallet-auth.ts
948
+ // ../../src/client/mcp-payment-server.ts
949
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
950
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
951
+ import {
952
+ CallToolRequestSchema,
953
+ ListToolsRequestSchema
954
+ } from "@modelcontextprotocol/sdk/types.js";
955
+
956
+ // ../../src/api/wallet-auth.ts
957
957
  import { createHash as createHash3 } from "node:crypto";
958
958
  import bs585 from "bs58";
959
959
  import nacl from "tweetnacl";
@@ -971,7 +971,7 @@ function walletAuthMessage(params) {
971
971
  );
972
972
  }
973
973
 
974
- // src/client/wallet-auth-headers.ts
974
+ // ../../src/client/wallet-auth-headers.ts
975
975
  async function walletAuthHeaders(params) {
976
976
  const signedAtMs = String(Date.now());
977
977
  const message = walletAuthMessage({
@@ -987,7 +987,7 @@ async function walletAuthHeaders(params) {
987
987
  };
988
988
  }
989
989
 
990
- // src/client/onboarding.ts
990
+ // ../../src/client/onboarding.ts
991
991
  var SELF_SERVE_POLICY_ID = "self-serve";
992
992
  var OnboardingError = class extends Error {
993
993
  constructor(step, message, detail = null) {
@@ -1044,187 +1044,7 @@ async function ensureWalletOnboarded(params) {
1044
1044
  await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1045
1045
  }
1046
1046
 
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
1047
+ // ../../src/x402/headers.ts
1228
1048
  import { z } from "zod";
1229
1049
  var PAYMENT_REQUIRED_HEADER = "payment-required";
1230
1050
  var MAX_HEADER_JSON_BYTES = 16384;
@@ -1291,7 +1111,17 @@ function decodeX402Header(headerValue) {
1291
1111
  }
1292
1112
  }
1293
1113
 
1294
- // src/x402/standard-requirements.ts
1114
+ // ../../src/client/paid-fetch.ts
1115
+ function formatRawUsdcAmount(raw) {
1116
+ const value = BigInt(raw);
1117
+ const negative = value < 0n;
1118
+ const abs = negative ? -value : value;
1119
+ const whole = abs / 1000000n;
1120
+ const frac = (abs % 1000000n).toString().padStart(6, "0");
1121
+ return `${negative ? "-" : ""}${whole}.${frac}`;
1122
+ }
1123
+
1124
+ // ../../src/x402/standard-requirements.ts
1295
1125
  import { z as z2 } from "zod";
1296
1126
  var STANDARD_EXACT_SCHEME = "exact";
1297
1127
  var standardExactRequirementSchema = z2.object({
@@ -1373,7 +1203,7 @@ function selectPayableSolanaRequirement(requirements, options) {
1373
1203
  };
1374
1204
  }
1375
1205
 
1376
- // src/client/standard-x402-payer.ts
1206
+ // ../../src/client/standard-x402-payer.ts
1377
1207
  var StandardX402PayError = class extends Error {
1378
1208
  constructor(reason, message, detail = null) {
1379
1209
  super(message);
@@ -1486,53 +1316,399 @@ var StandardX402Payer = class {
1486
1316
  }
1487
1317
  };
1488
1318
 
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");
1319
+ // ../../src/client/mcp-payment-server.ts
1320
+ var TOOL_NAME = "fetch_with_subly_payment";
1321
+ 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.
1322
+
1323
+ Before payments can succeed the operator of this server must have, once:
1324
+ 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.
1325
+ 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.
1326
+ 3. Waited for yield to accrue; a payment needs the seller's price of spendable yield.
1327
+
1328
+ Then use fetch_with_subly_payment(url) to GET or POST 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.`;
1329
+ async function runMcpPaymentServer(config) {
1330
+ const { payer: payer2, signer: signer2, facilitatorBaseUrl: facilitatorBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1331
+ const server = new Server(
1332
+ { name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
1333
+ { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1334
+ );
1335
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
1336
+ tools: [
1337
+ {
1338
+ name: TOOL_NAME,
1339
+ 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(
1340
+ defaultMaxAmountRawUsdc2
1341
+ )} 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.`,
1342
+ inputSchema: {
1343
+ type: "object",
1344
+ properties: {
1345
+ url: {
1346
+ type: "string",
1347
+ description: "URL to fetch. Must match the seller's resource URL exactly."
1348
+ },
1349
+ method: {
1350
+ type: "string",
1351
+ description: "HTTP method (default GET). Some x402 sellers deliver the paid resource over POST (e.g. an API that takes a JSON body)."
1352
+ },
1353
+ body: {
1354
+ type: "string",
1355
+ 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."
1356
+ },
1357
+ headers: {
1358
+ type: "object",
1359
+ description: "Extra request headers (object of string values), merged into both the probe and the paid retry.",
1360
+ additionalProperties: { type: "string" }
1361
+ },
1362
+ maxAmountRawUsdc: {
1363
+ type: "string",
1364
+ 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.'
1365
+ }
1366
+ },
1367
+ required: ["url"]
1368
+ },
1369
+ annotations: {
1370
+ title: "Fetch with Subly payment",
1371
+ readOnlyHint: false,
1372
+ destructiveHint: true,
1373
+ idempotentHint: false,
1374
+ openWorldHint: true
1375
+ }
1376
+ }
1377
+ ]
1378
+ }));
1379
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1380
+ if (request.params.name !== TOOL_NAME) {
1381
+ return {
1382
+ content: [{ type: "text", text: `unknown tool: ${request.params.name}` }],
1383
+ isError: true
1384
+ };
1385
+ }
1386
+ const args = request.params.arguments ?? {};
1387
+ const url = args.url;
1388
+ if (typeof url !== "string" || url.length === 0) {
1389
+ return {
1390
+ content: [{ type: "text", text: "missing required argument: url" }],
1391
+ isError: true
1392
+ };
1393
+ }
1394
+ let maxAmountRawUsdc;
1395
+ if (args.maxAmountRawUsdc !== void 0) {
1396
+ const raw = args.maxAmountRawUsdc;
1397
+ try {
1398
+ if (typeof raw !== "string" && typeof raw !== "number") {
1399
+ throw new TypeError("not a string or number");
1400
+ }
1401
+ maxAmountRawUsdc = BigInt(raw);
1402
+ } catch {
1403
+ return {
1404
+ content: [
1405
+ {
1406
+ type: "text",
1407
+ text: "maxAmountRawUsdc must be an integer raw USDC amount"
1408
+ }
1409
+ ],
1410
+ isError: true
1411
+ };
1412
+ }
1413
+ }
1414
+ const method = typeof args.method === "string" ? args.method : void 0;
1415
+ const body = typeof args.body === "string" ? args.body : void 0;
1416
+ const headers = args.headers !== null && typeof args.headers === "object" && !Array.isArray(args.headers) ? Object.fromEntries(
1417
+ Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
1418
+ ) : void 0;
1419
+ const mergedHeaders = body === void 0 ? headers : { "content-type": "application/json", ...headers ?? {} };
1420
+ try {
1421
+ const result = await payer2.pay({
1422
+ url,
1423
+ ...method === void 0 ? {} : { method },
1424
+ ...body === void 0 ? {} : { body },
1425
+ ...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
1426
+ ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
1427
+ });
1428
+ return {
1429
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1430
+ };
1431
+ } catch (error) {
1432
+ if (error instanceof StandardX402PayError) {
1433
+ return {
1434
+ content: [
1435
+ {
1436
+ type: "text",
1437
+ text: JSON.stringify(
1438
+ {
1439
+ paid: false,
1440
+ refused: true,
1441
+ reason: error.reason,
1442
+ message: error.message,
1443
+ detail: error.detail
1444
+ },
1445
+ null,
1446
+ 2
1447
+ )
1448
+ }
1449
+ ],
1450
+ isError: true
1451
+ };
1452
+ }
1453
+ return {
1454
+ content: [
1455
+ {
1456
+ type: "text",
1457
+ text: error instanceof Error ? error.message : String(error)
1458
+ }
1459
+ ],
1460
+ isError: true
1461
+ };
1462
+ }
1463
+ });
1464
+ try {
1465
+ await ensureWalletOnboarded({ facilitatorBaseUrl: facilitatorBaseUrl2, signer: signer2 });
1466
+ console.error("[subly-mcp] wallet registered and synced at the relayer");
1467
+ } catch (error) {
1468
+ console.error(
1469
+ `[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
1470
+ );
1494
1471
  }
1495
- const keypair = Keypair.fromSecretKey(secretKey);
1496
- return {
1497
- address: keypair.publicKey.toBase58(),
1498
- signTransaction: async (tx) => {
1499
- tx.sign([keypair]);
1500
- return tx;
1472
+ const transport = new StdioServerTransport();
1473
+ await server.connect(transport);
1474
+ console.error(
1475
+ `[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${facilitatorBaseUrl2}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
1476
+ );
1477
+ }
1478
+
1479
+ // ../../src/client/relayer-yield-realizer.ts
1480
+ import { address as address2 } from "@solana/kit";
1481
+
1482
+ // ../../src/client/lookup-tables.ts
1483
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1484
+ import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
1485
+ function lookupTableAddressesForTransaction(serializedTransaction) {
1486
+ const wire = Buffer.from(serializedTransaction, "base64");
1487
+ let offset = 0;
1488
+ let signatureCount = 0;
1489
+ let shift = 0;
1490
+ while (offset < wire.length) {
1491
+ const byte = wire[offset];
1492
+ signatureCount |= (byte & 127) << shift;
1493
+ offset += 1;
1494
+ if ((byte & 128) === 0) {
1495
+ break;
1501
1496
  }
1502
- };
1497
+ shift += 7;
1498
+ }
1499
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
1500
+ const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
1501
+ const lookups = compiled.addressTableLookups ?? [];
1502
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
1503
+ }
1504
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1505
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1506
+ if (addresses.length === 0) {
1507
+ return {};
1508
+ }
1509
+ const tables = await fetchAllMaybeAddressLookupTable(
1510
+ rpc2,
1511
+ addresses.map((value) => address(value))
1512
+ );
1513
+ const result = {};
1514
+ for (const table of tables) {
1515
+ if (table.exists) {
1516
+ result[table.address] = table.data.addresses.map(String);
1517
+ }
1518
+ }
1519
+ return result;
1503
1520
  }
1504
1521
 
1505
- // src/client/standard-x402-factory.ts
1506
- function createStandardX402Payer(config) {
1522
+ // ../../src/client/relayer-yield-realizer.ts
1523
+ var RelayerRealizeError = class extends Error {
1524
+ constructor(code, message, detail = null) {
1525
+ super(message);
1526
+ this.code = code;
1527
+ this.detail = detail;
1528
+ this.name = "RelayerRealizeError";
1529
+ }
1530
+ code;
1531
+ detail;
1532
+ };
1533
+ var RelayerYieldRealizer = class {
1534
+ config;
1535
+ signer;
1536
+ rpc;
1537
+ fetchImpl;
1538
+ lookupTablesFor;
1539
+ constructor(config) {
1540
+ this.config = {
1541
+ facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
1542
+ usdcMint: config.usdcMint ?? SUBLY_VAULT.usdcMint,
1543
+ forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
1544
+ };
1545
+ this.signer = config.signer;
1546
+ this.rpc = config.rpc;
1547
+ this.fetchImpl = config.fetchImpl ?? fetch;
1548
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
1549
+ }
1550
+ async ensureUsdcAvailable(input) {
1551
+ const agentAta = deriveAssociatedTokenAddress({
1552
+ owner: this.signer.walletAddress,
1553
+ mint: this.config.usdcMint
1554
+ });
1555
+ let shortfallRawUsdc;
1556
+ if (this.config.forceRealizeFullAmount) {
1557
+ shortfallRawUsdc = input.amountRawUsdc;
1558
+ } else {
1559
+ const currentBalance = await this.readTokenBalance(agentAta);
1560
+ if (currentBalance >= input.amountRawUsdc) {
1561
+ return { realizedRawUsdc: 0n, txSignature: null };
1562
+ }
1563
+ shortfallRawUsdc = input.amountRawUsdc - currentBalance;
1564
+ }
1565
+ await this.assertSpendableYield(shortfallRawUsdc);
1566
+ const prepared = await this.postJson("/v1/withdrawals/prepare", {
1567
+ wallet: this.signer.walletAddress,
1568
+ amountRawUsdc: shortfallRawUsdc.toString()
1569
+ });
1570
+ const signed = await this.signer.signWithdrawal({
1571
+ intent: prepared.signingIntent,
1572
+ serializedTransaction: prepared.serializedTransaction,
1573
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1574
+ });
1575
+ let settled = await this.postJson("/v1/withdrawals/submit", {
1576
+ withdrawalId: prepared.withdrawalId,
1577
+ serializedTransaction: signed.serializedTransaction,
1578
+ agentSignature: signed.agentSignature
1579
+ });
1580
+ if (settled.status === "submitted") {
1581
+ settled = await this.pollUntilSettled(prepared.withdrawalId);
1582
+ }
1583
+ if (settled.status !== "confirmed" || settled.txSignature === null) {
1584
+ throw new RelayerRealizeError(
1585
+ "realize_not_confirmed",
1586
+ `yield realize withdrawal did not confirm (status=${settled.status})`,
1587
+ settled
1588
+ );
1589
+ }
1590
+ return {
1591
+ realizedRawUsdc: BigInt(settled.actualWithdrawRawUsdc ?? "0"),
1592
+ txSignature: settled.txSignature
1593
+ };
1594
+ }
1595
+ /** Refuses to realize more than the ledger's spendable yield (principal). */
1596
+ async assertSpendableYield(shortfallRawUsdc) {
1597
+ const url = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1598
+ let response;
1599
+ try {
1600
+ response = await this.fetchImpl(url, {
1601
+ headers: await walletAuthHeaders({
1602
+ signer: this.signer,
1603
+ method: "GET",
1604
+ url
1605
+ })
1606
+ });
1607
+ } catch (error) {
1608
+ throw new RelayerRealizeError(
1609
+ "budget_unavailable",
1610
+ "could not read the spendable-yield budget",
1611
+ error
1612
+ );
1613
+ }
1614
+ if (response.status !== 200) {
1615
+ throw new RelayerRealizeError(
1616
+ "budget_unavailable",
1617
+ `budget endpoint returned ${response.status}`
1618
+ );
1619
+ }
1620
+ const body = await response.json();
1621
+ const spendable = BigInt(body.budget?.spendableYieldRawUsdc ?? "0");
1622
+ if (spendable < shortfallRawUsdc) {
1623
+ throw new RelayerRealizeError(
1624
+ "insufficient_yield",
1625
+ `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
1626
+ { spendableYieldRawUsdc: spendable.toString() }
1627
+ );
1628
+ }
1629
+ }
1630
+ /**
1631
+ * Polls GET /v1/withdrawals/:id (which reconciles a submitted intent against
1632
+ * the chain) until it leaves the "submitted" state or the timeout elapses.
1633
+ */
1634
+ async pollUntilSettled(withdrawalId, { timeoutMs = 9e4, intervalMs = 2500 } = {}) {
1635
+ const deadline = Date.now() + timeoutMs;
1636
+ let latest = null;
1637
+ while (Date.now() < deadline) {
1638
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1639
+ const url = `${this.config.facilitatorBaseUrl}/v1/withdrawals/${withdrawalId}`;
1640
+ const response = await this.fetchImpl(url, {
1641
+ headers: await walletAuthHeaders({
1642
+ signer: this.signer,
1643
+ method: "GET",
1644
+ url
1645
+ })
1646
+ });
1647
+ if (response.status !== 200) {
1648
+ continue;
1649
+ }
1650
+ latest = await response.json();
1651
+ if (latest.status !== "submitted") {
1652
+ return latest;
1653
+ }
1654
+ }
1655
+ return latest ?? {
1656
+ status: "submitted",
1657
+ txSignature: null,
1658
+ actualWithdrawRawUsdc: null
1659
+ };
1660
+ }
1661
+ async postJson(path, body) {
1662
+ const url = `${this.config.facilitatorBaseUrl}${path}`;
1663
+ const serialized = JSON.stringify(body);
1664
+ const response = await this.fetchImpl(url, {
1665
+ method: "POST",
1666
+ headers: {
1667
+ ...await walletAuthHeaders({
1668
+ signer: this.signer,
1669
+ method: "POST",
1670
+ url,
1671
+ body: serialized
1672
+ }),
1673
+ "content-type": "application/json"
1674
+ },
1675
+ body: serialized
1676
+ });
1677
+ const text = await response.text();
1678
+ if (response.status !== 200) {
1679
+ throw new RelayerRealizeError(
1680
+ path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
1681
+ `${path} failed with ${response.status}: ${text}`
1682
+ );
1683
+ }
1684
+ return JSON.parse(text);
1685
+ }
1686
+ async readTokenBalance(ata) {
1687
+ try {
1688
+ const response = await this.rpc.getTokenAccountBalance(address2(ata), { commitment: "confirmed" }).send();
1689
+ return BigInt(response.value.amount);
1690
+ } catch {
1691
+ return 0n;
1692
+ }
1693
+ }
1694
+ };
1695
+
1696
+ // ../../src/client/relayer-payer.ts
1697
+ function createRelayerX402Payer(config) {
1507
1698
  const realizer = new RelayerYieldRealizer({
1508
1699
  facilitatorBaseUrl: config.facilitatorBaseUrl,
1509
1700
  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
1701
+ rpc: config.rpc,
1702
+ forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
1516
1703
  });
1517
- const x402Fetch = (url, init) => client.fetch(url, init);
1518
1704
  return new StandardX402Payer({
1519
1705
  realizer,
1520
- x402Fetch,
1706
+ x402Fetch: config.x402Fetch,
1521
1707
  defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
1522
1708
  });
1523
1709
  }
1524
1710
 
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
1711
+ // ../../src/solana/keys.ts
1536
1712
  import { readFileSync } from "node:fs";
1537
1713
  import bs586 from "bs58";
1538
1714
  import {
@@ -1575,18 +1751,35 @@ function loadSecretKeyBytes(params) {
1575
1751
  throw new Error(`${label} keypair is not configured`);
1576
1752
  }
1577
1753
 
1578
- // src/solana/rpc.ts
1754
+ // ../../src/solana/rpc.ts
1579
1755
  import { createSolanaRpc } from "@solana/kit";
1580
1756
  function createRpc(url) {
1581
1757
  return createSolanaRpc(url);
1582
1758
  }
1583
1759
 
1584
- // demo/mcp-server.ts
1585
- var TOOL_NAME = "fetch_with_subly_payment";
1586
- var DEFAULT_MAX_AMOUNT_RAW_USDC = 10000n;
1760
+ // src/svm-x402-fetch.ts
1761
+ import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
1762
+ import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
1763
+ import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
1764
+ async function createSvmX402Fetch(params) {
1765
+ const signer2 = toClientSvmSigner(
1766
+ await createKeyPairSignerFromBytes2(params.agentSecretKey)
1767
+ );
1768
+ const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
1769
+ schemes: [
1770
+ {
1771
+ network: "solana:*",
1772
+ client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
1773
+ }
1774
+ ]
1775
+ });
1776
+ return (url, init) => wrapped(url, init);
1777
+ }
1778
+
1779
+ // src/mcp-server.ts
1587
1780
  var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1588
1781
  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);
1782
+ var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
1590
1783
  var keyPairSigner = await loadKeyPairSigner({
1591
1784
  base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1592
1785
  jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
@@ -1599,145 +1792,16 @@ var agentSecretKey = loadSecretKeyBytes({
1599
1792
  });
1600
1793
  var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1601
1794
  var rpc = createRpc(rpcUrl);
1602
- var payer = createStandardX402Payer({
1795
+ var payer = createRelayerX402Payer({
1603
1796
  facilitatorBaseUrl,
1604
1797
  signer,
1605
- agentSecretKey,
1606
1798
  rpc,
1607
- rpcUrl,
1799
+ x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
1608
1800
  defaultMaxAmountRawUsdc
1609
1801
  });
1610
- 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
-
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.
1616
-
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: [
1624
- {
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.`,
1629
- inputSchema: {
1630
- type: "object",
1631
- properties: {
1632
- url: {
1633
- type: "string",
1634
- description: "URL to fetch (GET). Must match the seller's resource URL exactly."
1635
- },
1636
- maxAmountRawUsdc: {
1637
- 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.'
1639
- }
1640
- },
1641
- required: ["url"]
1642
- },
1643
- annotations: {
1644
- title: "Fetch with Subly payment",
1645
- readOnlyHint: false,
1646
- destructiveHint: true,
1647
- idempotentHint: false,
1648
- openWorldHint: true
1649
- }
1650
- }
1651
- ]
1652
- }));
1653
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1654
- if (request.params.name !== TOOL_NAME) {
1655
- return {
1656
- content: [
1657
- { type: "text", text: `unknown tool: ${request.params.name}` }
1658
- ],
1659
- isError: true
1660
- };
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;
1673
- try {
1674
- if (typeof raw !== "string" && typeof raw !== "number") {
1675
- throw new TypeError("not a string or number");
1676
- }
1677
- maxAmountRawUsdc = BigInt(raw);
1678
- } catch {
1679
- return {
1680
- content: [
1681
- {
1682
- type: "text",
1683
- text: "maxAmountRawUsdc must be an integer raw USDC amount"
1684
- }
1685
- ],
1686
- isError: true
1687
- };
1688
- }
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) {
1700
- return {
1701
- content: [
1702
- {
1703
- 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
- )
1715
- }
1716
- ],
1717
- isError: true
1718
- };
1719
- }
1720
- return {
1721
- content: [
1722
- {
1723
- type: "text",
1724
- text: error instanceof Error ? error.message : String(error)
1725
- }
1726
- ],
1727
- isError: true
1728
- };
1729
- }
1802
+ await runMcpPaymentServer({
1803
+ payer,
1804
+ signer,
1805
+ facilitatorBaseUrl,
1806
+ defaultMaxAmountRawUsdc
1730
1807
  });
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)}`
1737
- );
1738
- }
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
- );