@subly_fi/pay 0.1.0 → 0.2.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,5 +1,4 @@
1
- // ../../demo/mcp-server.ts
2
- import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
1
+ // demo/mcp-server.ts
3
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
4
  import {
@@ -7,11 +6,11 @@ import {
7
6
  ListToolsRequestSchema
8
7
  } from "@modelcontextprotocol/sdk/types.js";
9
8
 
10
- // ../../src/client/agent-wallet-signer.ts
9
+ // src/client/agent-wallet-signer.ts
11
10
  import { signBytes } from "@solana/kit";
12
11
  import bs584 from "bs58";
13
12
 
14
- // ../../src/solana/tx.ts
13
+ // src/solana/tx.ts
15
14
  import bs58 from "bs58";
16
15
  import {
17
16
  appendTransactionMessageInstructions,
@@ -26,9 +25,8 @@ import {
26
25
  setTransactionMessageLifetimeUsingBlockhash
27
26
  } from "@solana/kit";
28
27
 
29
- // ../../src/lib/hash.ts
28
+ // src/lib/hash.ts
30
29
  import { createHash } from "node:crypto";
31
- var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
32
30
  function sha256TaggedHex(data) {
33
31
  return `sha256-${createHash("sha256").update(data).digest("hex")}`;
34
32
  }
@@ -52,7 +50,7 @@ function hashStableJson(value) {
52
50
  return sha256TaggedHex(stableStringify(value));
53
51
  }
54
52
 
55
- // ../../src/solana/tx.ts
53
+ // src/solana/tx.ts
56
54
  function decodeSerializedTransaction(serializedBase64) {
57
55
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
58
56
  }
@@ -72,11 +70,11 @@ function signatureBase58ForSigner(transaction, signer2) {
72
70
  return bs58.encode(signature);
73
71
  }
74
72
 
75
- // ../../src/client/transaction-intent-validator.ts
73
+ // src/client/transaction-intent-validator.ts
76
74
  import bs583 from "bs58";
77
75
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
78
76
 
79
- // ../../src/config/constants.ts
77
+ // src/config/constants.ts
80
78
  var PAYMENT_SCHEME = "subly-yield-exact";
81
79
  var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
82
80
  var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
@@ -92,7 +90,7 @@ var SUBLY_VAULT = {
92
90
  };
93
91
  var USDC_DECIMALS = 6;
94
92
 
95
- // ../../src/domain/request-binding.ts
93
+ // src/domain/request-binding.ts
96
94
  function computeRequestBindingHash(fields) {
97
95
  return hashStableJson({
98
96
  sellerRequestId: fields.sellerRequestId,
@@ -107,7 +105,7 @@ function computeRequestBindingHash(fields) {
107
105
  });
108
106
  }
109
107
 
110
- // ../../src/lib/associated-token-account.ts
108
+ // src/lib/associated-token-account.ts
111
109
  import { createHash as createHash2 } from "node:crypto";
112
110
  import bs582 from "bs58";
113
111
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
@@ -125,12 +123,12 @@ function deriveAssociatedTokenAddress(params) {
125
123
  "associatedTokenProgramId"
126
124
  );
127
125
  for (let bump = 255; bump >= 0; bump -= 1) {
128
- const address2 = createProgramAddress(
126
+ const address3 = createProgramAddress(
129
127
  [owner, tokenProgramId, mint, Uint8Array.of(bump)],
130
128
  associatedTokenProgramId
131
129
  );
132
- if (address2 !== null) {
133
- return bs582.encode(address2);
130
+ if (address3 !== null) {
131
+ return bs582.encode(address3);
134
132
  }
135
133
  }
136
134
  throw new Error("Unable to derive associated token account address");
@@ -199,7 +197,7 @@ function modPow(base, exponent, modulus) {
199
197
  return result;
200
198
  }
201
199
 
202
- // ../../src/client/transaction-intent-validator.ts
200
+ // src/client/transaction-intent-validator.ts
203
201
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
204
202
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
205
203
  var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
@@ -885,7 +883,7 @@ function readShortVec(bytes, startOffset) {
885
883
  return null;
886
884
  }
887
885
 
888
- // ../../src/client/agent-wallet-signer.ts
886
+ // src/client/agent-wallet-signer.ts
889
887
  var LocalKeypairAgentWalletSigner = class {
890
888
  validationMode = "structured_intent_transaction";
891
889
  keyPairSigner;
@@ -955,7 +953,104 @@ var LocalKeypairAgentWalletSigner = class {
955
953
  }
956
954
  };
957
955
 
958
- // ../../src/client/lookup-tables.ts
956
+ // src/api/wallet-auth.ts
957
+ import { createHash as createHash3 } from "node:crypto";
958
+ import bs585 from "bs58";
959
+ import nacl from "tweetnacl";
960
+ var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
961
+ var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
962
+ var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
963
+ function sha256Hex(data) {
964
+ return createHash3("sha256").update(data, "utf8").digest("hex");
965
+ }
966
+ function walletAuthMessage(params) {
967
+ return new TextEncoder().encode(
968
+ `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
969
+ params.rawBody
970
+ )}:${params.signedAtMs}`
971
+ );
972
+ }
973
+
974
+ // src/client/wallet-auth-headers.ts
975
+ async function walletAuthHeaders(params) {
976
+ const signedAtMs = String(Date.now());
977
+ const message = walletAuthMessage({
978
+ method: params.method,
979
+ path: new URL(params.url).pathname,
980
+ rawBody: params.body ?? "",
981
+ signedAtMs
982
+ });
983
+ return {
984
+ [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
985
+ [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
986
+ [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
987
+ };
988
+ }
989
+
990
+ // src/client/onboarding.ts
991
+ var SELF_SERVE_POLICY_ID = "self-serve";
992
+ var OnboardingError = class extends Error {
993
+ constructor(step, message, detail = null) {
994
+ super(message);
995
+ this.step = step;
996
+ this.detail = detail;
997
+ this.name = "OnboardingError";
998
+ }
999
+ step;
1000
+ detail;
1001
+ };
1002
+ async function ensureWalletOnboarded(params) {
1003
+ const fetchImpl = params.fetchImpl ?? fetch;
1004
+ const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
1005
+ const post = async (step, path, body) => {
1006
+ const url = `${baseUrl}${path}`;
1007
+ const serialized = JSON.stringify(body);
1008
+ const response = await fetchImpl(url, {
1009
+ method: "POST",
1010
+ headers: {
1011
+ ...await walletAuthHeaders({
1012
+ signer: params.signer,
1013
+ method: "POST",
1014
+ url,
1015
+ body: serialized
1016
+ }),
1017
+ "content-type": "application/json"
1018
+ },
1019
+ body: serialized
1020
+ });
1021
+ if (response.status !== 200) {
1022
+ let detail = null;
1023
+ try {
1024
+ detail = await response.json();
1025
+ } catch {
1026
+ detail = null;
1027
+ }
1028
+ throw new OnboardingError(
1029
+ step,
1030
+ `wallet onboarding ${step} failed with ${response.status}`,
1031
+ detail
1032
+ );
1033
+ }
1034
+ };
1035
+ const wallet = params.signer.walletAddress;
1036
+ await post("register", "/v1/wallets/agent", {
1037
+ wallet,
1038
+ signingPolicyId: SELF_SERVE_POLICY_ID,
1039
+ signingMode: "non_interactive",
1040
+ signerValidationMode: params.signer.validationMode,
1041
+ signerProvider: "local-keypair",
1042
+ activateForPayments: true
1043
+ });
1044
+ await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1045
+ }
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
959
1054
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
960
1055
  import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
961
1056
  function lookupTableAddressesForTransaction(serializedTransaction) {
@@ -995,12 +1090,143 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
995
1090
  return result;
996
1091
  }
997
1092
 
998
- // ../../src/x402/headers.ts
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
999
1228
  import { z } from "zod";
1000
1229
  var PAYMENT_REQUIRED_HEADER = "payment-required";
1001
- var PAYMENT_SIGNATURE_HEADER = "payment-signature";
1002
- var PAYMENT_RESPONSE_HEADER = "payment-response";
1003
- var X402_VERSION = 2;
1004
1230
  var MAX_HEADER_JSON_BYTES = 16384;
1005
1231
  var X402HeaderError = class extends Error {
1006
1232
  reason;
@@ -1047,16 +1273,6 @@ var sublyPaymentPayloadSchema = z.object({
1047
1273
  temporarySettlementSignature: z.string().min(1).max(128)
1048
1274
  })
1049
1275
  }).loose();
1050
- function encodeX402Header(value) {
1051
- const json = JSON.stringify(value);
1052
- if (Buffer.byteLength(json, "utf8") > MAX_HEADER_JSON_BYTES) {
1053
- throw new X402HeaderError(
1054
- "header_too_large",
1055
- `x402 header JSON exceeds ${MAX_HEADER_JSON_BYTES} bytes`
1056
- );
1057
- }
1058
- return Buffer.from(json, "utf8").toString("base64");
1059
- }
1060
1276
  function decodeX402Header(headerValue) {
1061
1277
  if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
1062
1278
  throw new X402HeaderError(
@@ -1074,375 +1290,249 @@ function decodeX402Header(headerValue) {
1074
1290
  );
1075
1291
  }
1076
1292
  }
1077
- function decodePaymentRequiredHeader(headerValue) {
1078
- const parsed = paymentRequiredSchema.safeParse(decodeX402Header(headerValue));
1293
+
1294
+ // src/x402/standard-requirements.ts
1295
+ import { z as z2 } from "zod";
1296
+ var STANDARD_EXACT_SCHEME = "exact";
1297
+ var standardExactRequirementSchema = z2.object({
1298
+ scheme: z2.literal(STANDARD_EXACT_SCHEME),
1299
+ /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
1300
+ network: z2.string().min(1),
1301
+ /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
1302
+ asset: z2.string().min(1),
1303
+ /** Exact price in the asset's atomic units, as a decimal string. */
1304
+ amount: z2.string().regex(/^[1-9]\d*$/),
1305
+ /** Recipient wallet; the transfer destination ATA is derived from it. */
1306
+ payTo: z2.string().min(1),
1307
+ maxTimeoutSeconds: z2.number().int().positive().optional(),
1308
+ extra: z2.object({
1309
+ /** Facilitator address that pays the tx fee (gas sponsorship). */
1310
+ feePayer: z2.string().min(1).optional()
1311
+ }).loose().optional()
1312
+ }).loose();
1313
+ var standardPaymentRequiredSchema = z2.object({
1314
+ x402Version: z2.number().int(),
1315
+ accepts: z2.array(z2.unknown()),
1316
+ error: z2.string().optional(),
1317
+ resource: z2.object({ url: z2.string().optional() }).loose().optional()
1318
+ }).loose();
1319
+ var StandardX402ChallengeError = class extends Error {
1320
+ reason;
1321
+ constructor(reason, message) {
1322
+ super(message);
1323
+ this.name = "StandardX402ChallengeError";
1324
+ this.reason = reason;
1325
+ }
1326
+ };
1327
+ function parseStandardChallenge(challenge) {
1328
+ const parsed = standardPaymentRequiredSchema.safeParse(challenge);
1079
1329
  if (!parsed.success) {
1080
- throw new X402HeaderError(
1330
+ throw new StandardX402ChallengeError(
1081
1331
  "invalid_payment_required",
1082
- "PAYMENT-REQUIRED header is not a valid x402 PaymentRequired object"
1332
+ "Response is not a valid x402 PaymentRequired object"
1083
1333
  );
1084
1334
  }
1085
- const sublyRequirements = parsed.data.accepts.flatMap((candidate) => {
1086
- const requirement = sublyPaymentRequirementsSchema.safeParse(candidate);
1087
- return requirement.success ? [requirement.data] : [];
1335
+ const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
1336
+ const requirement = standardExactRequirementSchema.safeParse(candidate);
1337
+ if (!requirement.success) {
1338
+ return [];
1339
+ }
1340
+ return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
1088
1341
  });
1089
- return { paymentRequired: parsed.data, sublyRequirements };
1342
+ return { paymentRequired: parsed.data, solanaExactRequirements };
1090
1343
  }
1091
- function requestBodyHashFor(body) {
1092
- if (body === null || body === void 0 || body.length === 0) {
1093
- return EMPTY_BODY_HASH;
1344
+ function decodeStandardPaymentRequiredHeader(headerValue) {
1345
+ let decoded;
1346
+ try {
1347
+ decoded = decodeX402Header(headerValue);
1348
+ } catch (error) {
1349
+ throw new StandardX402ChallengeError(
1350
+ error instanceof X402HeaderError ? error.reason : "invalid_header",
1351
+ "Cannot decode the payment-required header"
1352
+ );
1094
1353
  }
1095
- return sha256TaggedHex(
1096
- typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1097
- );
1354
+ return parseStandardChallenge(decoded);
1355
+ }
1356
+ function selectPayableSolanaRequirement(requirements, options) {
1357
+ const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1358
+ const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1359
+ const requirement = requirements.find(
1360
+ (candidate) => candidate.network === network && candidate.asset === usdcMint
1361
+ ) ?? null;
1362
+ if (requirement === null) {
1363
+ throw new StandardX402ChallengeError(
1364
+ "no_payable_requirement",
1365
+ `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1366
+ );
1367
+ }
1368
+ return {
1369
+ requirement,
1370
+ amountRawUsdc: BigInt(requirement.amount),
1371
+ payTo: requirement.payTo,
1372
+ feePayer: requirement.extra?.feePayer ?? null
1373
+ };
1098
1374
  }
1099
1375
 
1100
- // ../../src/client/paid-fetch.ts
1101
- var PaidFetchError = class extends Error {
1376
+ // src/client/standard-x402-payer.ts
1377
+ var StandardX402PayError = class extends Error {
1102
1378
  constructor(reason, message, detail = null) {
1103
1379
  super(message);
1104
1380
  this.reason = reason;
1105
1381
  this.detail = detail;
1106
- this.name = "PaidFetchError";
1382
+ this.name = "StandardX402PayError";
1107
1383
  }
1108
1384
  reason;
1109
1385
  detail;
1110
1386
  };
1111
- var DEFAULT_PENDING_TTL_MS = 11e4;
1112
- var DEFAULT_MAX_TRACKED_URLS = 1e3;
1113
- var DEFAULT_MAX_BODY_CHARS = 2e4;
1114
- function formatRawUsdcAmount(raw) {
1115
- const value = BigInt(raw);
1116
- const negative = value < 0n;
1117
- const abs = negative ? -value : value;
1118
- const whole = abs / 1000000n;
1119
- const frac = (abs % 1000000n).toString().padStart(6, "0");
1120
- return `${negative ? "-" : ""}${whole}.${frac}`;
1121
- }
1122
- var PaidFetchService = class {
1123
- signatureBuilder;
1124
- fetchImpl;
1125
- fetchBudget;
1126
- paymentStatusFor;
1387
+ var StandardX402Payer = class {
1388
+ realizer;
1389
+ x402Fetch;
1390
+ probeFetch;
1127
1391
  defaultMaxAmountRawUsdc;
1128
- pendingTtlMs;
1129
- maxTrackedUrls;
1130
- maxBodyChars;
1131
- nowMs;
1132
- stateStore;
1133
- pending = /* @__PURE__ */ new Map();
1134
- inFlight = /* @__PURE__ */ new Map();
1392
+ network;
1393
+ usdcMint;
1135
1394
  constructor(config) {
1136
- this.signatureBuilder = config.signatureBuilder;
1137
- this.fetchImpl = config.fetchImpl ?? fetch;
1138
- this.fetchBudget = config.fetchBudget ?? (async () => null);
1139
- this.paymentStatusFor = config.paymentStatusFor ?? (async () => "indeterminate");
1395
+ this.realizer = config.realizer;
1396
+ this.x402Fetch = config.x402Fetch;
1397
+ this.probeFetch = config.probeFetch ?? fetch;
1140
1398
  this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1141
- this.pendingTtlMs = config.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
1142
- this.maxTrackedUrls = config.maxTrackedUrls ?? DEFAULT_MAX_TRACKED_URLS;
1143
- this.maxBodyChars = config.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS;
1144
- this.nowMs = config.nowMs ?? (() => Date.now());
1145
- this.stateStore = config.stateStore ?? null;
1146
- if (this.stateStore !== null) {
1147
- for (const record of this.stateStore.load()) {
1148
- const { url, ...entry } = record;
1149
- this.pending.set(url, entry);
1150
- }
1151
- }
1152
- }
1153
- /**
1154
- * GET the URL, paying a 402 challenge when needed. Concurrent calls for the
1155
- * same URL share one flow and one result.
1156
- */
1157
- paidFetch(params) {
1158
- const existing = this.inFlight.get(params.url);
1159
- if (existing !== void 0) {
1160
- return existing;
1161
- }
1162
- const flow = this.run(params).finally(() => {
1163
- this.inFlight.delete(params.url);
1164
- });
1165
- this.inFlight.set(params.url, flow);
1166
- return flow;
1167
- }
1168
- async run(params) {
1169
- const { url } = params;
1170
- const pending = this.pending.get(url);
1171
- if (pending !== void 0) {
1172
- const expired = this.nowMs() - pending.challengeAtMs > this.pendingTtlMs;
1173
- if (pending.unresolved || expired) {
1174
- if (params.forceNewPayment === true) {
1175
- this.untrack(url);
1176
- } else {
1177
- const outcome = await this.paymentStatusFor(pending.paymentId);
1178
- if (outcome === "not_settled") {
1179
- this.untrack(url);
1180
- } else if (outcome === "settled") {
1181
- throw new PaidFetchError(
1182
- "payment_already_settled",
1183
- `the previous payment for this URL settled (paymentId=${pending.paymentId}) but the content delivery was lost, and the signature can no longer be retried. Calling again with forceNewPayment=true will PAY A SECOND TIME for the same resource.`,
1184
- { paymentId: pending.paymentId }
1185
- );
1186
- } else {
1187
- throw new PaidFetchError(
1188
- "payment_outcome_unknown",
1189
- `a previously signed payment for this URL (paymentId=${pending.paymentId}) has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call this tool with forceNewPayment=true.`,
1190
- { paymentId: pending.paymentId }
1191
- );
1192
- }
1193
- }
1194
- } else {
1195
- return this.deliver(url, pending, {
1196
- retried: true,
1197
- budgetBefore: null
1198
- });
1199
- }
1200
- }
1201
- const challengeAtMs = this.nowMs();
1202
- const first = await this.fetchImpl(url);
1203
- const firstText = await first.text();
1204
- if (first.status !== 402) {
1205
- return {
1206
- paid: false,
1207
- status: first.status,
1208
- body: this.truncated(firstText)
1209
- };
1210
- }
1211
- const challengeHeader = first.headers.get(PAYMENT_REQUIRED_HEADER);
1212
- if (challengeHeader === null) {
1213
- throw new PaidFetchError(
1214
- "invalid_challenge",
1215
- "402 response is missing the PAYMENT-REQUIRED header"
1216
- );
1217
- }
1218
- const requirement = decodePaymentRequiredHeader(challengeHeader).sublyRequirements[0];
1219
- if (requirement === void 0) {
1220
- throw new PaidFetchError(
1221
- "invalid_challenge",
1222
- "402 challenge contains no subly-yield-exact requirement"
1223
- );
1399
+ this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1400
+ this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1401
+ }
1402
+ async pay(input) {
1403
+ const init = {
1404
+ method: input.method ?? "GET",
1405
+ ...input.body === void 0 ? {} : { body: input.body },
1406
+ ...input.headers === void 0 ? {} : { headers: input.headers }
1407
+ };
1408
+ const probe = await this.probeFetch(input.url, init);
1409
+ if (probe.status !== 402) {
1410
+ return { paid: false, status: probe.status, body: await probe.text() };
1224
1411
  }
1225
- const amountRawUsdc = BigInt(requirement.amountRawUsdc);
1226
- const maxAmountRawUsdc = params.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1227
- if (amountRawUsdc > maxAmountRawUsdc) {
1228
- throw new PaidFetchError(
1412
+ const selected = await this.selectRequirement(probe);
1413
+ const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1414
+ if (selected.amountRawUsdc > cap) {
1415
+ throw new StandardX402PayError(
1229
1416
  "amount_exceeds_client_cap",
1230
- `the challenge demands ${formatRawUsdcAmount(amountRawUsdc)} USDC, above this tool call's cap of ${formatRawUsdcAmount(maxAmountRawUsdc)} USDC; nothing was paid. Raise maxAmountRawUsdc only if this price is expected.`,
1231
- {
1232
- amountRawUsdc: requirement.amountRawUsdc,
1233
- maxAmountRawUsdc: maxAmountRawUsdc.toString(),
1234
- payTo: requirement.payTo
1235
- }
1417
+ `the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
1418
+ { amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
1236
1419
  );
1237
1420
  }
1238
- const budgetBefore = await this.fetchBudget();
1239
- const { headerValue, paymentId } = await this.signatureBuilder.buildPaymentSignatureHeader({
1240
- paymentRequiredHeader: challengeHeader,
1241
- httpMethod: "GET",
1242
- url
1243
- });
1244
- const entry = {
1245
- headerValue,
1246
- paymentId,
1247
- amountUsdc: formatRawUsdcAmount(amountRawUsdc),
1248
- payTo: requirement.payTo,
1249
- challengeAtMs,
1250
- unresolved: false
1251
- };
1252
- this.track(url, entry);
1253
- return this.deliver(url, entry, { retried: false, budgetBefore });
1254
- }
1255
- /**
1256
- * Sends the signed PAYMENT-SIGNATURE retry. The tracked entry is removed
1257
- * only on confirmed delivery (200). A fresh 402 proves the signature can no
1258
- * longer settle, so the entry is kept as an unresolved marker; every other
1259
- * failure keeps it retryable so the next call reuses the same signature.
1260
- */
1261
- async deliver(url, entry, params) {
1262
- let second;
1263
- let secondText;
1421
+ let realized;
1264
1422
  try {
1265
- second = await this.fetchImpl(url, {
1266
- headers: { [PAYMENT_SIGNATURE_HEADER]: entry.headerValue }
1423
+ realized = await this.realizer.ensureUsdcAvailable({
1424
+ amountRawUsdc: selected.amountRawUsdc
1267
1425
  });
1268
- secondText = await second.text();
1269
1426
  } catch (error) {
1270
- throw new PaidFetchError(
1271
- "delivery_failed_payment_pending",
1272
- `delivery request failed in flight (${error instanceof Error ? error.message : String(error)}); the payment (paymentId=${entry.paymentId}) is already signed and may settle. Call this tool again with the same URL to retry delivery with the same payment signature \u2014 do NOT treat this as unpaid.`,
1273
- { paymentId: entry.paymentId }
1427
+ throw new StandardX402PayError(
1428
+ "realize_failed",
1429
+ `could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
1430
+ error
1274
1431
  );
1275
1432
  }
1276
- if (second.status === 200) {
1277
- this.untrack(url);
1278
- const transaction = receiptTransaction(second.headers);
1279
- return {
1280
- paid: true,
1281
- status: second.status,
1282
- body: this.truncated(secondText),
1283
- ...params.retried ? { retriedPendingPayment: true } : {},
1284
- payment: {
1285
- amountUsdc: entry.amountUsdc,
1286
- payTo: entry.payTo,
1287
- paymentId: entry.paymentId,
1288
- transaction,
1289
- solscanUrl: transaction === null ? null : `https://solscan.io/tx/${transaction}`,
1290
- budgetBefore: params.budgetBefore,
1291
- budgetAfter: await this.fetchBudget()
1292
- }
1293
- };
1294
- }
1295
- if (second.status === 402) {
1296
- entry.unresolved = true;
1297
- this.persist();
1298
- throw new PaidFetchError(
1299
- "payment_outcome_unknown",
1300
- `the seller no longer accepts the signed payment (paymentId=${entry.paymentId}); it may or may not have settled. Verify the payment before purchasing again; to pay again anyway, call this tool with forceNewPayment=true.`,
1301
- { paymentId: entry.paymentId }
1433
+ const response = await this.x402Fetch(input.url, init);
1434
+ const bodyText = await response.text();
1435
+ if (response.status !== 200) {
1436
+ throw new StandardX402PayError(
1437
+ "delivery_failed",
1438
+ `the x402 payment did not deliver (status ${response.status})`,
1439
+ { status: response.status, body: bodyText }
1302
1440
  );
1303
1441
  }
1304
- throw new PaidFetchError(
1305
- "delivery_failed_payment_pending",
1306
- `paid delivery failed with ${second.status} (paymentId=${entry.paymentId}); the payment may already have settled. Call this tool again with the same URL to retry delivery with the same payment signature \u2014 do NOT treat this as unpaid.`,
1307
- {
1308
- paymentId: entry.paymentId,
1309
- status: second.status,
1310
- body: this.truncated(secondText)
1442
+ return {
1443
+ paid: true,
1444
+ status: response.status,
1445
+ body: bodyText,
1446
+ payment: {
1447
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1448
+ payTo: selected.payTo,
1449
+ feePayer: selected.feePayer,
1450
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1451
+ realizeTxSignature: realized.txSignature
1311
1452
  }
1312
- );
1453
+ };
1313
1454
  }
1314
- track(url, entry) {
1315
- if (!this.pending.has(url) && this.pending.size >= this.maxTrackedUrls) {
1316
- const oldest = this.pending.keys().next();
1317
- if (!oldest.done) {
1318
- this.pending.delete(oldest.value);
1455
+ /** Reads the challenge from the header (preferred) or the JSON body. */
1456
+ async selectRequirement(probe) {
1457
+ const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
1458
+ let requirements;
1459
+ try {
1460
+ if (header !== null) {
1461
+ requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
1462
+ } else {
1463
+ requirements = parseStandardChallenge(
1464
+ await probe.json()
1465
+ ).solanaExactRequirements;
1319
1466
  }
1467
+ } catch (error) {
1468
+ throw new StandardX402PayError(
1469
+ error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
1470
+ "could not parse the x402 402 challenge",
1471
+ error
1472
+ );
1320
1473
  }
1321
- this.pending.set(url, entry);
1322
- this.persist();
1323
- }
1324
- untrack(url) {
1325
- this.pending.delete(url);
1326
- this.persist();
1327
- }
1328
- persist() {
1329
- if (this.stateStore === null) {
1330
- return;
1474
+ try {
1475
+ return selectPayableSolanaRequirement(requirements, {
1476
+ network: this.network,
1477
+ usdcMint: this.usdcMint
1478
+ });
1479
+ } catch (error) {
1480
+ throw new StandardX402PayError(
1481
+ "no_payable_requirement",
1482
+ error instanceof Error ? error.message : String(error),
1483
+ error
1484
+ );
1331
1485
  }
1332
- this.stateStore.save(
1333
- [...this.pending.entries()].map(([url, entry]) => ({ url, ...entry }))
1334
- );
1335
- }
1336
- truncated(text) {
1337
- return text.length > this.maxBodyChars ? `${text.slice(0, this.maxBodyChars)}
1338
- ... (truncated)` : text;
1339
1486
  }
1340
1487
  };
1341
- function receiptTransaction(headers) {
1342
- try {
1343
- const receiptHeader = headers.get(PAYMENT_RESPONSE_HEADER);
1344
- if (receiptHeader === null) {
1345
- return null;
1346
- }
1347
- const receipt = decodeX402Header(receiptHeader);
1348
- return typeof receipt.transaction === "string" && receipt.transaction.length > 0 ? receipt.transaction : null;
1349
- } catch {
1350
- return null;
1351
- }
1352
- }
1353
1488
 
1354
- // ../../src/api/wallet-auth.ts
1355
- import { createHash as createHash3 } from "node:crypto";
1356
- import bs585 from "bs58";
1357
- import nacl from "tweetnacl";
1358
- var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
1359
- var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
1360
- var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
1361
- function sha256Hex(data) {
1362
- return createHash3("sha256").update(data, "utf8").digest("hex");
1363
- }
1364
- function walletAuthMessage(params) {
1365
- return new TextEncoder().encode(
1366
- `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
1367
- params.rawBody
1368
- )}:${params.signedAtMs}`
1369
- );
1370
- }
1371
-
1372
- // ../../src/client/wallet-auth-headers.ts
1373
- async function walletAuthHeaders(params) {
1374
- const signedAtMs = String(Date.now());
1375
- const message = walletAuthMessage({
1376
- method: params.method,
1377
- path: new URL(params.url).pathname,
1378
- rawBody: params.body ?? "",
1379
- signedAtMs
1380
- });
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");
1494
+ }
1495
+ const keypair = Keypair.fromSecretKey(secretKey);
1381
1496
  return {
1382
- [WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
1383
- [WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
1384
- [WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
1497
+ address: keypair.publicKey.toBase58(),
1498
+ signTransaction: async (tx) => {
1499
+ tx.sign([keypair]);
1500
+ return tx;
1501
+ }
1385
1502
  };
1386
1503
  }
1387
1504
 
1388
- // ../../src/client/onboarding.ts
1389
- var SELF_SERVE_POLICY_ID = "self-serve";
1390
- var OnboardingError = class extends Error {
1391
- constructor(step, message, detail = null) {
1392
- super(message);
1393
- this.step = step;
1394
- this.detail = detail;
1395
- this.name = "OnboardingError";
1396
- }
1397
- step;
1398
- detail;
1399
- };
1400
- async function ensureWalletOnboarded(params) {
1401
- const fetchImpl = params.fetchImpl ?? fetch;
1402
- const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
1403
- const post = async (step, path, body) => {
1404
- const url = `${baseUrl}${path}`;
1405
- const serialized = JSON.stringify(body);
1406
- const response = await fetchImpl(url, {
1407
- method: "POST",
1408
- headers: {
1409
- ...await walletAuthHeaders({
1410
- signer: params.signer,
1411
- method: "POST",
1412
- url,
1413
- body: serialized
1414
- }),
1415
- "content-type": "application/json"
1416
- },
1417
- body: serialized
1418
- });
1419
- if (response.status !== 200) {
1420
- let detail = null;
1421
- try {
1422
- detail = await response.json();
1423
- } catch {
1424
- detail = null;
1425
- }
1426
- throw new OnboardingError(
1427
- step,
1428
- `wallet onboarding ${step} failed with ${response.status}`,
1429
- detail
1430
- );
1431
- }
1432
- };
1433
- const wallet = params.signer.walletAddress;
1434
- await post("register", "/v1/wallets/agent", {
1435
- wallet,
1436
- signingPolicyId: SELF_SERVE_POLICY_ID,
1437
- signingMode: "non_interactive",
1438
- signerValidationMode: params.signer.validationMode,
1439
- signerProvider: "local-keypair",
1440
- activateForPayments: true
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
1441
1522
  });
1442
- await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1443
1523
  }
1444
1524
 
1445
- // ../../src/solana/keys.ts
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
1446
1536
  import { readFileSync } from "node:fs";
1447
1537
  import bs586 from "bs58";
1448
1538
  import {
@@ -1466,314 +1556,76 @@ async function loadKeyPairSigner(params) {
1466
1556
  }
1467
1557
  throw new Error(`${label} keypair is not configured`);
1468
1558
  }
1469
-
1470
- // ../../src/solana/rpc.ts
1471
- import { createSolanaRpc } from "@solana/kit";
1472
- function createRpcFromEnv(env = process.env) {
1473
- const url = env.SOLANA_RPC_URL;
1474
- if (url === void 0 || url.length === 0) {
1475
- throw new Error("SOLANA_RPC_URL must be configured");
1476
- }
1477
- return createSolanaRpc(url);
1478
- }
1479
-
1480
- // ../../src/x402/client.ts
1481
- var X402ClientError = class extends Error {
1482
- reason;
1483
- detail;
1484
- constructor(reason, message, detail) {
1485
- super(message);
1486
- this.name = "X402ClientError";
1487
- this.reason = reason;
1488
- this.detail = detail;
1489
- }
1490
- };
1491
- var SublyX402Client = class {
1492
- config;
1493
- signer;
1494
- lookupTablesFor;
1495
- fetchImpl;
1496
- constructor(config) {
1497
- this.config = {
1498
- facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
1499
- network: config.network ?? SOLANA_MAINNET_NETWORK
1500
- };
1501
- this.signer = config.signer;
1502
- this.lookupTablesFor = config.lookupTablesFor ?? null;
1503
- this.fetchImpl = config.fetchImpl ?? fetch;
1504
- }
1505
- /**
1506
- * Builds the PAYMENT-SIGNATURE header for a 402 challenge. The request
1507
- * method, URL, and body must be exactly the request being retried; they are
1508
- * bound into the payment and verified again by the seller and facilitator.
1509
- */
1510
- async buildPaymentSignatureHeader(input) {
1511
- const requirement = this.selectRequirement(input.paymentRequiredHeader);
1512
- if (requirement.resource !== input.url) {
1513
- throw new X402ClientError(
1514
- "resource_mismatch",
1515
- "PAYMENT-REQUIRED resource does not match the request URL"
1516
- );
1517
- }
1518
- const requestBodyHash = requestBodyHashFor(input.body);
1519
- const prepared = await this.preparePayment({
1520
- requirement,
1521
- httpMethod: input.httpMethod.toUpperCase(),
1522
- requestBodyHash
1523
- });
1524
- const lookupTables = this.lookupTablesFor === null ? void 0 : await this.lookupTablesFor(prepared.intentJson.serializedTransaction);
1525
- const signed = await this.signer.signPayment({
1526
- intent: prepared.intentJson.signingIntent,
1527
- serializedTransaction: prepared.intentJson.serializedTransaction,
1528
- lookupTables
1529
- });
1530
- const payload = {
1531
- x402Version: X402_VERSION,
1532
- scheme: requirement.scheme,
1533
- network: requirement.network,
1534
- payload: {
1535
- paymentId: prepared.paymentId,
1536
- requestBindingHash: prepared.requestBindingHash,
1537
- preparedMessageHash: prepared.preparedMessageHash,
1538
- serializedTransaction: signed.serializedTransaction,
1539
- agentSignature: signed.agentSignature,
1540
- temporarySettlementSignature: prepared.temporarySettlementSignature
1541
- }
1542
- };
1543
- return {
1544
- headerValue: encodeX402Header(payload),
1545
- paymentId: prepared.paymentId
1546
- };
1547
- }
1548
- /**
1549
- * Convenience wrapper: performs the request, and on a 402 with a Subly
1550
- * requirement pays from vault yield and retries once.
1551
- */
1552
- async fetchWithPayment(url, init) {
1553
- const first = await this.fetchImpl(url, init);
1554
- if (first.status !== 402) {
1555
- return first;
1556
- }
1557
- const challenge = first.headers.get(PAYMENT_REQUIRED_HEADER);
1558
- if (challenge === null) {
1559
- return first;
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`);
1560
1565
  }
1561
- const { headerValue } = await this.buildPaymentSignatureHeader({
1562
- paymentRequiredHeader: challenge,
1563
- httpMethod: init?.method ?? "GET",
1564
- url,
1565
- body: init?.body ?? null
1566
- });
1567
- return this.fetchImpl(url, {
1568
- ...init,
1569
- headers: {
1570
- ...init?.headers,
1571
- [PAYMENT_SIGNATURE_HEADER]: headerValue
1572
- }
1573
- });
1566
+ return bytes;
1574
1567
  }
1575
- selectRequirement(paymentRequiredHeader) {
1576
- let sublyRequirements;
1577
- try {
1578
- sublyRequirements = decodePaymentRequiredHeader(
1579
- paymentRequiredHeader
1580
- ).sublyRequirements;
1581
- } catch (error) {
1582
- throw new X402ClientError(
1583
- error instanceof X402HeaderError ? error.reason : "invalid_challenge",
1584
- "Cannot decode the PAYMENT-REQUIRED header"
1585
- );
1586
- }
1587
- const selected = sublyRequirements.find(
1588
- (candidate) => candidate.network === this.config.network && candidate.extra.vault === SUBLY_VAULT.address && candidate.extra.shareMint === SUBLY_VAULT.shareMint && candidate.asset === SUBLY_VAULT.usdcMint
1589
- ) ?? null;
1590
- if (selected === null) {
1591
- throw new X402ClientError(
1592
- "no_supported_requirement",
1593
- "The 402 challenge contains no supported subly-yield-exact requirement"
1594
- );
1595
- }
1596
- return selected;
1597
- }
1598
- async preparePayment(input) {
1599
- const { requirement } = input;
1600
- const wallet = this.signer.walletAddress;
1601
- const prepareUrl = `${this.config.facilitatorBaseUrl}/v1/payments/prepare`;
1602
- const prepareBody = JSON.stringify({
1603
- wallet,
1604
- scheme: requirement.scheme,
1605
- network: requirement.network,
1606
- vault: requirement.extra.vault,
1607
- shareMint: requirement.extra.shareMint,
1608
- asset: requirement.asset,
1609
- seller: requirement.extra.seller,
1610
- sellerRequestId: requirement.extra.sellerRequestId,
1611
- httpMethod: input.httpMethod,
1612
- canonicalResourceUrl: requirement.resource,
1613
- requestBodyHash: input.requestBodyHash,
1614
- amountRawUsdc: requirement.amountRawUsdc,
1615
- payTo: requirement.payTo,
1616
- sellerUsdcAta: requirement.extra.sellerUsdcAta,
1617
- dustRecipientUsdcAta: deriveAssociatedTokenAddress({
1618
- owner: wallet,
1619
- mint: SUBLY_VAULT.usdcMint
1620
- })
1621
- });
1622
- const response = await this.fetchImpl(prepareUrl, {
1623
- method: "POST",
1624
- headers: {
1625
- ...await walletAuthHeaders({
1626
- signer: this.signer,
1627
- method: "POST",
1628
- url: prepareUrl,
1629
- body: prepareBody
1630
- }),
1631
- "content-type": "application/json"
1632
- },
1633
- body: prepareBody
1634
- });
1635
- const body = await response.json();
1636
- if (response.status !== 200) {
1637
- const error = body.error;
1638
- throw new X402ClientError(
1639
- typeof error?.code === "string" ? error.code : "prepare_failed",
1640
- "Payment preparation failed at the facilitator",
1641
- body
1642
- );
1643
- }
1644
- const prepared = body;
1645
- if (typeof prepared.paymentId !== "string" || typeof prepared.preparedMessageHash !== "string" || typeof prepared.intentJson?.serializedTransaction !== "string" || typeof prepared.intentJson?.signingIntent !== "object") {
1646
- throw new X402ClientError(
1647
- "invalid_prepare_response",
1648
- "Facilitator prepare response is missing required fields"
1649
- );
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`);
1650
1572
  }
1651
- return prepared;
1573
+ return Uint8Array.from(raw);
1652
1574
  }
1653
- };
1654
-
1655
- // ../../demo/shared.ts
1656
- function fail(message) {
1657
- console.error(message);
1658
- process.exit(1);
1575
+ throw new Error(`${label} keypair is not configured`);
1659
1576
  }
1660
- function requireEnv(name) {
1661
- const value = process.env[name];
1662
- if (value === void 0 || value.length === 0) {
1663
- fail(`Missing required environment variable: ${name}`);
1664
- }
1665
- return value;
1577
+
1578
+ // src/solana/rpc.ts
1579
+ import { createSolanaRpc } from "@solana/kit";
1580
+ function createRpc(url) {
1581
+ return createSolanaRpc(url);
1666
1582
  }
1667
1583
 
1668
- // ../../demo/mcp-server.ts
1584
+ // demo/mcp-server.ts
1669
1585
  var TOOL_NAME = "fetch_with_subly_payment";
1670
1586
  var DEFAULT_MAX_AMOUNT_RAW_USDC = 10000n;
1671
- requireEnv("SOLANA_RPC_URL");
1672
- var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "http://localhost:3000";
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";
1673
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);
1674
1590
  var keyPairSigner = await loadKeyPairSigner({
1675
1591
  base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1676
1592
  jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1677
1593
  label: "SUBLY_DEMO_AGENT_KEYPAIR"
1678
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
+ });
1679
1600
  var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1680
- var rpc = createRpcFromEnv();
1681
- async function fetchBudget() {
1682
- try {
1683
- const url = `${facilitatorBaseUrl}/v1/wallets/${signer.walletAddress}/budget`;
1684
- const response = await fetch(url, {
1685
- headers: await walletAuthHeaders({ signer, method: "GET", url })
1686
- });
1687
- if (response.status !== 200) {
1688
- return null;
1689
- }
1690
- const body = await response.json();
1691
- if (body.budget === void 0) {
1692
- return null;
1693
- }
1694
- return {
1695
- positionValueUsdc: formatRawUsdcAmount(body.budget.positionValueRawUsdc),
1696
- spendableYieldUsdc: formatRawUsdcAmount(
1697
- body.budget.spendableYieldRawUsdc
1698
- )
1699
- };
1700
- } catch {
1701
- return null;
1702
- }
1703
- }
1704
- async function paymentStatusFor(paymentId) {
1705
- try {
1706
- const url = `${facilitatorBaseUrl}/v1/payments/${paymentId}`;
1707
- const response = await fetch(url, {
1708
- headers: await walletAuthHeaders({ signer, method: "GET", url })
1709
- });
1710
- if (response.status !== 200) {
1711
- return "indeterminate";
1712
- }
1713
- const body = await response.json();
1714
- if (body.status === "settled") {
1715
- return "settled";
1716
- }
1717
- if (body.status === "expired" || body.status === "failed" || body.status === "failed_not_submitted") {
1718
- return "not_settled";
1719
- }
1720
- return "indeterminate";
1721
- } catch {
1722
- return "indeterminate";
1723
- }
1724
- }
1725
- function fileStateStore(path) {
1726
- return {
1727
- load() {
1728
- try {
1729
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
1730
- return Array.isArray(parsed) ? parsed : [];
1731
- } catch {
1732
- return [];
1733
- }
1734
- },
1735
- save(records) {
1736
- try {
1737
- writeFileSync(path, JSON.stringify(records));
1738
- } catch (error) {
1739
- console.error(
1740
- `[subly-mcp] failed to persist pending payments: ${error instanceof Error ? error.message : String(error)}`
1741
- );
1742
- }
1743
- }
1744
- };
1745
- }
1746
- var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? "demo/env/mcp-pending-payments.json";
1747
- var paidFetchService = new PaidFetchService({
1748
- signatureBuilder: new SublyX402Client({
1749
- facilitatorBaseUrl,
1750
- signer,
1751
- lookupTablesFor: (serializedTransaction) => fetchLookupTablesForTransaction(rpc, serializedTransaction)
1752
- }),
1753
- defaultMaxAmountRawUsdc,
1754
- fetchBudget,
1755
- paymentStatusFor,
1756
- stateStore: fileStateStore(pendingStatePath)
1601
+ var rpc = createRpc(rpcUrl);
1602
+ var payer = createStandardX402Payer({
1603
+ facilitatorBaseUrl,
1604
+ signer,
1605
+ agentSecretKey,
1606
+ rpc,
1607
+ rpcUrl,
1608
+ defaultMaxAmountRawUsdc
1757
1609
  });
1758
- var SERVER_INSTRUCTIONS = `Subly lets an agent pay for HTTP 402 (subly-yield-exact) resources from its wallet's Kamino vault YIELD \u2014 the deposited principal is never spent.
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.
1759
1611
 
1760
1612
  Before payments can succeed the operator of this server must have, once:
1761
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.
1762
- 2. Funded that wallet with USDC on Solana mainnet (no SOL needed \u2014 fees are sponsored) and deposited into the vault (see the project's deposit command). The vault minimum deposit is 1 USDC.
1763
- 3. Waited for yield to accrue; a payment needs the price plus a fixed overhead (~0.0024 USDC) of spendable yield.
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.
1764
1616
 
1765
- Then use fetch_with_subly_payment(url) to GET a paid resource: it pays the 402 from yield and returns the body plus an on-chain receipt. If it returns insufficient_yield, that is expected \u2014 wait for yield, do not loop. If it returns delivery_failed_payment_pending, call the SAME url again (it retries the same payment, never double-pays).`;
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.`;
1766
1618
  var server = new Server(
1767
- { name: "subly-payments", version: "0.1.0" },
1619
+ { name: "subly-payments", version: "0.2.0" },
1768
1620
  { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1769
1621
  );
1770
1622
  server.setRequestHandler(ListToolsRequestSchema, () => ({
1771
1623
  tools: [
1772
1624
  {
1773
1625
  name: TOOL_NAME,
1774
- description: `GET a URL, automatically paying a Subly x402 (subly-yield-exact) 402 challenge from the agent wallet's Kamino vault yield. Returns the response body; when a payment was made, also returns the settlement receipt (amount, payee, paymentId, Solscan link). Challenges above maxAmountRawUsdc (default ${defaultMaxAmountRawUsdc} raw = ${formatRawUsdcAmount(
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(
1775
1627
  defaultMaxAmountRawUsdc
1776
- )} USDC) are refused without paying. If delivery fails after the payment was signed, the signature is kept and calling again with the same URL retries the same payment instead of paying twice. Payments are refused by the facilitator when the spendable yield budget cannot cover them \u2014 the principal is never spent. Use only for URLs you intend to purchase access to.`,
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.`,
1777
1629
  inputSchema: {
1778
1630
  type: "object",
1779
1631
  properties: {
@@ -1784,10 +1636,6 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
1784
1636
  maxAmountRawUsdc: {
1785
1637
  type: "string",
1786
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.'
1787
- },
1788
- forceNewPayment: {
1789
- type: "boolean",
1790
- description: "Pay again even though a previous payment for this URL settled or has an unknown outcome. Only set deliberately: combined with payment_already_settled this means paying twice for the same resource. Ignored while the previous payment is still retryable (the same signature is retried instead)."
1791
1639
  }
1792
1640
  },
1793
1641
  required: ["url"]
@@ -1840,16 +1688,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1840
1688
  }
1841
1689
  }
1842
1690
  try {
1843
- const result = await paidFetchService.paidFetch({
1691
+ const result = await payer.pay({
1844
1692
  url,
1845
- ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
1846
- forceNewPayment: args.forceNewPayment === true
1693
+ ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
1847
1694
  });
1848
1695
  return {
1849
1696
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1850
1697
  };
1851
1698
  } catch (error) {
1852
- if (error instanceof PaidFetchError) {
1699
+ if (error instanceof StandardX402PayError) {
1853
1700
  return {
1854
1701
  content: [
1855
1702
  {
@@ -1870,26 +1717,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1870
1717
  isError: true
1871
1718
  };
1872
1719
  }
1873
- if (error instanceof X402ClientError) {
1874
- return {
1875
- content: [
1876
- {
1877
- type: "text",
1878
- text: JSON.stringify(
1879
- {
1880
- paid: false,
1881
- refused: true,
1882
- reason: error.reason,
1883
- detail: error.detail ?? null
1884
- },
1885
- null,
1886
- 2
1887
- )
1888
- }
1889
- ],
1890
- isError: true
1891
- };
1892
- }
1893
1720
  return {
1894
1721
  content: [
1895
1722
  {
@@ -1903,7 +1730,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1903
1730
  });
1904
1731
  try {
1905
1732
  await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
1906
- console.error("[subly-mcp] wallet registered and synced at the facilitator");
1733
+ console.error("[subly-mcp] wallet registered and synced at the relayer");
1907
1734
  } catch (error) {
1908
1735
  console.error(
1909
1736
  `[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
@@ -1912,5 +1739,5 @@ try {
1912
1739
  var transport = new StdioServerTransport();
1913
1740
  await server.connect(transport);
1914
1741
  console.error(
1915
- `[subly-mcp] ready: agent wallet ${signer.walletAddress}, facilitator ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc)} USDC`
1742
+ `[subly-mcp] ready: agent wallet ${signer.walletAddress}, relayer ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc)} USDC`
1916
1743
  );