@subly_fi/pay 0.1.1 → 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.
Files changed (3) hide show
  1. package/dist/mcp-server.js +678 -771
  2. package/dist/pay.js +456 -556
  3. package/package.json +10 -6
@@ -1,12 +1,3 @@
1
- // ../../demo/mcp-server.ts
2
- import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema
8
- } from "@modelcontextprotocol/sdk/types.js";
9
-
10
1
  // ../../src/client/agent-wallet-signer.ts
11
2
  import { signBytes } from "@solana/kit";
12
3
  import bs584 from "bs58";
@@ -28,7 +19,6 @@ import {
28
19
 
29
20
  // ../../src/lib/hash.ts
30
21
  import { createHash } from "node:crypto";
31
- var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
32
22
  function sha256TaggedHex(data) {
33
23
  return `sha256-${createHash("sha256").update(data).digest("hex")}`;
34
24
  }
@@ -125,12 +115,12 @@ function deriveAssociatedTokenAddress(params) {
125
115
  "associatedTokenProgramId"
126
116
  );
127
117
  for (let bump = 255; bump >= 0; bump -= 1) {
128
- const address2 = createProgramAddress(
118
+ const address3 = createProgramAddress(
129
119
  [owner, tokenProgramId, mint, Uint8Array.of(bump)],
130
120
  associatedTokenProgramId
131
121
  );
132
- if (address2 !== null) {
133
- return bs582.encode(address2);
122
+ if (address3 !== null) {
123
+ return bs582.encode(address3);
134
124
  }
135
125
  }
136
126
  throw new Error("Unable to derive associated token account address");
@@ -955,52 +945,108 @@ var LocalKeypairAgentWalletSigner = class {
955
945
  }
956
946
  };
957
947
 
958
- // ../../src/client/lookup-tables.ts
959
- import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
960
- import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
961
- function lookupTableAddressesForTransaction(serializedTransaction) {
962
- const wire = Buffer.from(serializedTransaction, "base64");
963
- let offset = 0;
964
- let signatureCount = 0;
965
- let shift = 0;
966
- while (offset < wire.length) {
967
- const byte = wire[offset];
968
- signatureCount |= (byte & 127) << shift;
969
- offset += 1;
970
- if ((byte & 128) === 0) {
971
- break;
972
- }
973
- shift += 7;
974
- }
975
- const messageBytes = wire.subarray(offset + signatureCount * 64);
976
- const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
977
- const lookups = compiled.addressTableLookups ?? [];
978
- return lookups.map((lookup) => String(lookup.lookupTableAddress));
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
+ 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");
979
965
  }
980
- async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
981
- const addresses = lookupTableAddressesForTransaction(serializedTransaction);
982
- if (addresses.length === 0) {
983
- return {};
984
- }
985
- const tables = await fetchAllMaybeAddressLookupTable(
986
- rpc2,
987
- addresses.map((value) => address(value))
966
+ function walletAuthMessage(params) {
967
+ return new TextEncoder().encode(
968
+ `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
969
+ params.rawBody
970
+ )}:${params.signedAtMs}`
988
971
  );
989
- const result = {};
990
- for (const table of tables) {
991
- if (table.exists) {
992
- result[table.address] = table.data.addresses.map(String);
993
- }
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";
994
998
  }
995
- return result;
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" });
996
1045
  }
997
1046
 
998
1047
  // ../../src/x402/headers.ts
999
1048
  import { z } from "zod";
1000
1049
  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
1050
  var MAX_HEADER_JSON_BYTES = 16384;
1005
1051
  var X402HeaderError = class extends Error {
1006
1052
  reason;
@@ -1047,16 +1093,6 @@ var sublyPaymentPayloadSchema = z.object({
1047
1093
  temporarySettlementSignature: z.string().min(1).max(128)
1048
1094
  })
1049
1095
  }).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
1096
  function decodeX402Header(headerValue) {
1061
1097
  if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
1062
1098
  throw new X402HeaderError(
@@ -1074,340 +1110,562 @@ function decodeX402Header(headerValue) {
1074
1110
  );
1075
1111
  }
1076
1112
  }
1077
- function decodePaymentRequiredHeader(headerValue) {
1078
- const parsed = paymentRequiredSchema.safeParse(decodeX402Header(headerValue));
1113
+
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
1125
+ import { z as z2 } from "zod";
1126
+ var STANDARD_EXACT_SCHEME = "exact";
1127
+ var standardExactRequirementSchema = z2.object({
1128
+ scheme: z2.literal(STANDARD_EXACT_SCHEME),
1129
+ /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
1130
+ network: z2.string().min(1),
1131
+ /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
1132
+ asset: z2.string().min(1),
1133
+ /** Exact price in the asset's atomic units, as a decimal string. */
1134
+ amount: z2.string().regex(/^[1-9]\d*$/),
1135
+ /** Recipient wallet; the transfer destination ATA is derived from it. */
1136
+ payTo: z2.string().min(1),
1137
+ maxTimeoutSeconds: z2.number().int().positive().optional(),
1138
+ extra: z2.object({
1139
+ /** Facilitator address that pays the tx fee (gas sponsorship). */
1140
+ feePayer: z2.string().min(1).optional()
1141
+ }).loose().optional()
1142
+ }).loose();
1143
+ var standardPaymentRequiredSchema = z2.object({
1144
+ x402Version: z2.number().int(),
1145
+ accepts: z2.array(z2.unknown()),
1146
+ error: z2.string().optional(),
1147
+ resource: z2.object({ url: z2.string().optional() }).loose().optional()
1148
+ }).loose();
1149
+ var StandardX402ChallengeError = class extends Error {
1150
+ reason;
1151
+ constructor(reason, message) {
1152
+ super(message);
1153
+ this.name = "StandardX402ChallengeError";
1154
+ this.reason = reason;
1155
+ }
1156
+ };
1157
+ function parseStandardChallenge(challenge) {
1158
+ const parsed = standardPaymentRequiredSchema.safeParse(challenge);
1079
1159
  if (!parsed.success) {
1080
- throw new X402HeaderError(
1160
+ throw new StandardX402ChallengeError(
1081
1161
  "invalid_payment_required",
1082
- "PAYMENT-REQUIRED header is not a valid x402 PaymentRequired object"
1162
+ "Response is not a valid x402 PaymentRequired object"
1083
1163
  );
1084
1164
  }
1085
- const sublyRequirements = parsed.data.accepts.flatMap((candidate) => {
1086
- const requirement = sublyPaymentRequirementsSchema.safeParse(candidate);
1087
- return requirement.success ? [requirement.data] : [];
1165
+ const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
1166
+ const requirement = standardExactRequirementSchema.safeParse(candidate);
1167
+ if (!requirement.success) {
1168
+ return [];
1169
+ }
1170
+ return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
1088
1171
  });
1089
- return { paymentRequired: parsed.data, sublyRequirements };
1172
+ return { paymentRequired: parsed.data, solanaExactRequirements };
1173
+ }
1174
+ function decodeStandardPaymentRequiredHeader(headerValue) {
1175
+ let decoded;
1176
+ try {
1177
+ decoded = decodeX402Header(headerValue);
1178
+ } catch (error) {
1179
+ throw new StandardX402ChallengeError(
1180
+ error instanceof X402HeaderError ? error.reason : "invalid_header",
1181
+ "Cannot decode the payment-required header"
1182
+ );
1183
+ }
1184
+ return parseStandardChallenge(decoded);
1090
1185
  }
1091
- function requestBodyHashFor(body) {
1092
- if (body === null || body === void 0 || body.length === 0) {
1093
- return EMPTY_BODY_HASH;
1186
+ function selectPayableSolanaRequirement(requirements, options) {
1187
+ const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1188
+ const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1189
+ const requirement = requirements.find(
1190
+ (candidate) => candidate.network === network && candidate.asset === usdcMint
1191
+ ) ?? null;
1192
+ if (requirement === null) {
1193
+ throw new StandardX402ChallengeError(
1194
+ "no_payable_requirement",
1195
+ `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1196
+ );
1094
1197
  }
1095
- return sha256TaggedHex(
1096
- typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1097
- );
1198
+ return {
1199
+ requirement,
1200
+ amountRawUsdc: BigInt(requirement.amount),
1201
+ payTo: requirement.payTo,
1202
+ feePayer: requirement.extra?.feePayer ?? null
1203
+ };
1098
1204
  }
1099
1205
 
1100
- // ../../src/client/paid-fetch.ts
1101
- var PaidFetchError = class extends Error {
1206
+ // ../../src/client/standard-x402-payer.ts
1207
+ var StandardX402PayError = class extends Error {
1102
1208
  constructor(reason, message, detail = null) {
1103
1209
  super(message);
1104
1210
  this.reason = reason;
1105
1211
  this.detail = detail;
1106
- this.name = "PaidFetchError";
1212
+ this.name = "StandardX402PayError";
1107
1213
  }
1108
1214
  reason;
1109
1215
  detail;
1110
1216
  };
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;
1217
+ var StandardX402Payer = class {
1218
+ realizer;
1219
+ x402Fetch;
1220
+ probeFetch;
1127
1221
  defaultMaxAmountRawUsdc;
1128
- pendingTtlMs;
1129
- maxTrackedUrls;
1130
- maxBodyChars;
1131
- nowMs;
1132
- stateStore;
1133
- pending = /* @__PURE__ */ new Map();
1134
- inFlight = /* @__PURE__ */ new Map();
1222
+ network;
1223
+ usdcMint;
1135
1224
  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");
1225
+ this.realizer = config.realizer;
1226
+ this.x402Fetch = config.x402Fetch;
1227
+ this.probeFetch = config.probeFetch ?? fetch;
1140
1228
  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
- };
1229
+ this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1230
+ this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1231
+ }
1232
+ async pay(input) {
1233
+ const init = {
1234
+ method: input.method ?? "GET",
1235
+ ...input.body === void 0 ? {} : { body: input.body },
1236
+ ...input.headers === void 0 ? {} : { headers: input.headers }
1237
+ };
1238
+ const probe = await this.probeFetch(input.url, init);
1239
+ if (probe.status !== 402) {
1240
+ return { paid: false, status: probe.status, body: await probe.text() };
1210
1241
  }
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"
1242
+ const selected = await this.selectRequirement(probe);
1243
+ const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1244
+ if (selected.amountRawUsdc > cap) {
1245
+ throw new StandardX402PayError(
1246
+ "amount_exceeds_client_cap",
1247
+ `the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
1248
+ { amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
1216
1249
  );
1217
1250
  }
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"
1251
+ let realized;
1252
+ try {
1253
+ realized = await this.realizer.ensureUsdcAvailable({
1254
+ amountRawUsdc: selected.amountRawUsdc
1255
+ });
1256
+ } catch (error) {
1257
+ throw new StandardX402PayError(
1258
+ "realize_failed",
1259
+ `could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
1260
+ error
1223
1261
  );
1224
1262
  }
1225
- const amountRawUsdc = BigInt(requirement.amountRawUsdc);
1226
- const maxAmountRawUsdc = params.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1227
- if (amountRawUsdc > maxAmountRawUsdc) {
1228
- throw new PaidFetchError(
1229
- "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
- }
1263
+ const response = await this.x402Fetch(input.url, init);
1264
+ const bodyText = await response.text();
1265
+ if (response.status !== 200) {
1266
+ throw new StandardX402PayError(
1267
+ "delivery_failed",
1268
+ `the x402 payment did not deliver (status ${response.status})`,
1269
+ { status: response.status, body: bodyText }
1236
1270
  );
1237
1271
  }
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
1272
+ return {
1273
+ paid: true,
1274
+ status: response.status,
1275
+ body: bodyText,
1276
+ payment: {
1277
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1278
+ payTo: selected.payTo,
1279
+ feePayer: selected.feePayer,
1280
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1281
+ realizeTxSignature: realized.txSignature
1282
+ }
1251
1283
  };
1252
- this.track(url, entry);
1253
- return this.deliver(url, entry, { retried: false, budgetBefore });
1254
1284
  }
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;
1285
+ /** Reads the challenge from the header (preferred) or the JSON body. */
1286
+ async selectRequirement(probe) {
1287
+ const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
1288
+ let requirements;
1289
+ try {
1290
+ if (header !== null) {
1291
+ requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
1292
+ } else {
1293
+ requirements = parseStandardChallenge(
1294
+ await probe.json()
1295
+ ).solanaExactRequirements;
1296
+ }
1297
+ } catch (error) {
1298
+ throw new StandardX402PayError(
1299
+ error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
1300
+ "could not parse the x402 402 challenge",
1301
+ error
1302
+ );
1303
+ }
1264
1304
  try {
1265
- second = await this.fetchImpl(url, {
1266
- headers: { [PAYMENT_SIGNATURE_HEADER]: entry.headerValue }
1305
+ return selectPayableSolanaRequirement(requirements, {
1306
+ network: this.network,
1307
+ usdcMint: this.usdcMint
1267
1308
  });
1268
- secondText = await second.text();
1269
1309
  } 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 }
1310
+ throw new StandardX402PayError(
1311
+ "no_payable_requirement",
1312
+ error instanceof Error ? error.message : String(error),
1313
+ error
1274
1314
  );
1275
1315
  }
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()
1316
+ }
1317
+ };
1318
+
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
1292
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
1293
1384
  };
1294
1385
  }
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 }
1302
- );
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
+ };
1303
1393
  }
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)
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
+ };
1311
1412
  }
1312
- );
1313
- }
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);
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
+ };
1319
1452
  }
1453
+ return {
1454
+ content: [
1455
+ {
1456
+ type: "text",
1457
+ text: error instanceof Error ? error.message : String(error)
1458
+ }
1459
+ ],
1460
+ isError: true
1461
+ };
1320
1462
  }
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;
1331
- }
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
- }
1340
- };
1341
- function receiptTransaction(headers) {
1463
+ });
1342
1464
  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;
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
+ );
1351
1471
  }
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
+ );
1352
1477
  }
1353
1478
 
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");
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;
1496
+ }
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));
1363
1503
  }
1364
- function walletAuthMessage(params) {
1365
- return new TextEncoder().encode(
1366
- `subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
1367
- params.rawBody
1368
- )}:${params.signedAtMs}`
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))
1369
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;
1370
1520
  }
1371
1521
 
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
- });
1381
- 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)
1385
- };
1386
- }
1387
-
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) {
1522
+ // ../../src/client/relayer-yield-realizer.ts
1523
+ var RelayerRealizeError = class extends Error {
1524
+ constructor(code, message, detail = null) {
1392
1525
  super(message);
1393
- this.step = step;
1526
+ this.code = code;
1394
1527
  this.detail = detail;
1395
- this.name = "OnboardingError";
1528
+ this.name = "RelayerRealizeError";
1396
1529
  }
1397
- step;
1530
+ code;
1398
1531
  detail;
1399
1532
  };
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}`;
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}`;
1405
1663
  const serialized = JSON.stringify(body);
1406
- const response = await fetchImpl(url, {
1664
+ const response = await this.fetchImpl(url, {
1407
1665
  method: "POST",
1408
1666
  headers: {
1409
1667
  ...await walletAuthHeaders({
1410
- signer: params.signer,
1668
+ signer: this.signer,
1411
1669
  method: "POST",
1412
1670
  url,
1413
1671
  body: serialized
@@ -1416,30 +1674,38 @@ async function ensureWalletOnboarded(params) {
1416
1674
  },
1417
1675
  body: serialized
1418
1676
  });
1677
+ const text = await response.text();
1419
1678
  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
1679
+ throw new RelayerRealizeError(
1680
+ path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
1681
+ `${path} failed with ${response.status}: ${text}`
1430
1682
  );
1431
1683
  }
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
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) {
1698
+ const realizer = new RelayerYieldRealizer({
1699
+ facilitatorBaseUrl: config.facilitatorBaseUrl,
1700
+ signer: config.signer,
1701
+ rpc: config.rpc,
1702
+ forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
1703
+ });
1704
+ return new StandardX402Payer({
1705
+ realizer,
1706
+ x402Fetch: config.x402Fetch,
1707
+ defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
1441
1708
  });
1442
- await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1443
1709
  }
1444
1710
 
1445
1711
  // ../../src/solana/keys.ts
@@ -1466,6 +1732,24 @@ async function loadKeyPairSigner(params) {
1466
1732
  }
1467
1733
  throw new Error(`${label} keypair is not configured`);
1468
1734
  }
1735
+ function loadSecretKeyBytes(params) {
1736
+ const { base58Secret, jsonFilePath, label } = params;
1737
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
1738
+ const bytes = bs586.decode(base58Secret);
1739
+ if (bytes.length !== 64) {
1740
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
1741
+ }
1742
+ return bytes;
1743
+ }
1744
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1745
+ const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1746
+ if (!Array.isArray(raw) || raw.length !== 64) {
1747
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1748
+ }
1749
+ return Uint8Array.from(raw);
1750
+ }
1751
+ throw new Error(`${label} keypair is not configured`);
1752
+ }
1469
1753
 
1470
1754
  // ../../src/solana/rpc.ts
1471
1755
  import { createSolanaRpc } from "@solana/kit";
@@ -1473,428 +1757,51 @@ function createRpc(url) {
1473
1757
  return createSolanaRpc(url);
1474
1758
  }
1475
1759
 
1476
- // ../../src/x402/client.ts
1477
- var X402ClientError = class extends Error {
1478
- reason;
1479
- detail;
1480
- constructor(reason, message, detail) {
1481
- super(message);
1482
- this.name = "X402ClientError";
1483
- this.reason = reason;
1484
- this.detail = detail;
1485
- }
1486
- };
1487
- var SublyX402Client = class {
1488
- config;
1489
- signer;
1490
- lookupTablesFor;
1491
- fetchImpl;
1492
- constructor(config) {
1493
- this.config = {
1494
- facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
1495
- network: config.network ?? SOLANA_MAINNET_NETWORK
1496
- };
1497
- this.signer = config.signer;
1498
- this.lookupTablesFor = config.lookupTablesFor ?? null;
1499
- this.fetchImpl = config.fetchImpl ?? fetch;
1500
- }
1501
- /**
1502
- * Builds the PAYMENT-SIGNATURE header for a 402 challenge. The request
1503
- * method, URL, and body must be exactly the request being retried; they are
1504
- * bound into the payment and verified again by the seller and facilitator.
1505
- */
1506
- async buildPaymentSignatureHeader(input) {
1507
- const requirement = this.selectRequirement(input.paymentRequiredHeader);
1508
- if (requirement.resource !== input.url) {
1509
- throw new X402ClientError(
1510
- "resource_mismatch",
1511
- "PAYMENT-REQUIRED resource does not match the request URL"
1512
- );
1513
- }
1514
- const requestBodyHash = requestBodyHashFor(input.body);
1515
- const prepared = await this.preparePayment({
1516
- requirement,
1517
- httpMethod: input.httpMethod.toUpperCase(),
1518
- requestBodyHash
1519
- });
1520
- const lookupTables = this.lookupTablesFor === null ? void 0 : await this.lookupTablesFor(prepared.intentJson.serializedTransaction);
1521
- const signed = await this.signer.signPayment({
1522
- intent: prepared.intentJson.signingIntent,
1523
- serializedTransaction: prepared.intentJson.serializedTransaction,
1524
- lookupTables
1525
- });
1526
- const payload = {
1527
- x402Version: X402_VERSION,
1528
- scheme: requirement.scheme,
1529
- network: requirement.network,
1530
- payload: {
1531
- paymentId: prepared.paymentId,
1532
- requestBindingHash: prepared.requestBindingHash,
1533
- preparedMessageHash: prepared.preparedMessageHash,
1534
- serializedTransaction: signed.serializedTransaction,
1535
- agentSignature: signed.agentSignature,
1536
- temporarySettlementSignature: prepared.temporarySettlementSignature
1537
- }
1538
- };
1539
- return {
1540
- headerValue: encodeX402Header(payload),
1541
- paymentId: prepared.paymentId
1542
- };
1543
- }
1544
- /**
1545
- * Convenience wrapper: performs the request, and on a 402 with a Subly
1546
- * requirement pays from vault yield and retries once.
1547
- */
1548
- async fetchWithPayment(url, init) {
1549
- const first = await this.fetchImpl(url, init);
1550
- if (first.status !== 402) {
1551
- return first;
1552
- }
1553
- const challenge = first.headers.get(PAYMENT_REQUIRED_HEADER);
1554
- if (challenge === null) {
1555
- return first;
1556
- }
1557
- const { headerValue } = await this.buildPaymentSignatureHeader({
1558
- paymentRequiredHeader: challenge,
1559
- httpMethod: init?.method ?? "GET",
1560
- url,
1561
- body: init?.body ?? null
1562
- });
1563
- return this.fetchImpl(url, {
1564
- ...init,
1565
- headers: {
1566
- ...init?.headers,
1567
- [PAYMENT_SIGNATURE_HEADER]: headerValue
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 })
1568
1773
  }
1569
- });
1570
- }
1571
- selectRequirement(paymentRequiredHeader) {
1572
- let sublyRequirements;
1573
- try {
1574
- sublyRequirements = decodePaymentRequiredHeader(
1575
- paymentRequiredHeader
1576
- ).sublyRequirements;
1577
- } catch (error) {
1578
- throw new X402ClientError(
1579
- error instanceof X402HeaderError ? error.reason : "invalid_challenge",
1580
- "Cannot decode the PAYMENT-REQUIRED header"
1581
- );
1582
- }
1583
- const selected = sublyRequirements.find(
1584
- (candidate) => candidate.network === this.config.network && candidate.extra.vault === SUBLY_VAULT.address && candidate.extra.shareMint === SUBLY_VAULT.shareMint && candidate.asset === SUBLY_VAULT.usdcMint
1585
- ) ?? null;
1586
- if (selected === null) {
1587
- throw new X402ClientError(
1588
- "no_supported_requirement",
1589
- "The 402 challenge contains no supported subly-yield-exact requirement"
1590
- );
1591
- }
1592
- return selected;
1593
- }
1594
- async preparePayment(input) {
1595
- const { requirement } = input;
1596
- const wallet = this.signer.walletAddress;
1597
- const prepareUrl = `${this.config.facilitatorBaseUrl}/v1/payments/prepare`;
1598
- const prepareBody = JSON.stringify({
1599
- wallet,
1600
- scheme: requirement.scheme,
1601
- network: requirement.network,
1602
- vault: requirement.extra.vault,
1603
- shareMint: requirement.extra.shareMint,
1604
- asset: requirement.asset,
1605
- seller: requirement.extra.seller,
1606
- sellerRequestId: requirement.extra.sellerRequestId,
1607
- httpMethod: input.httpMethod,
1608
- canonicalResourceUrl: requirement.resource,
1609
- requestBodyHash: input.requestBodyHash,
1610
- amountRawUsdc: requirement.amountRawUsdc,
1611
- payTo: requirement.payTo,
1612
- sellerUsdcAta: requirement.extra.sellerUsdcAta,
1613
- dustRecipientUsdcAta: deriveAssociatedTokenAddress({
1614
- owner: wallet,
1615
- mint: SUBLY_VAULT.usdcMint
1616
- })
1617
- });
1618
- const response = await this.fetchImpl(prepareUrl, {
1619
- method: "POST",
1620
- headers: {
1621
- ...await walletAuthHeaders({
1622
- signer: this.signer,
1623
- method: "POST",
1624
- url: prepareUrl,
1625
- body: prepareBody
1626
- }),
1627
- "content-type": "application/json"
1628
- },
1629
- body: prepareBody
1630
- });
1631
- const body = await response.json();
1632
- if (response.status !== 200) {
1633
- const error = body.error;
1634
- throw new X402ClientError(
1635
- typeof error?.code === "string" ? error.code : "prepare_failed",
1636
- "Payment preparation failed at the facilitator",
1637
- body
1638
- );
1639
- }
1640
- const prepared = body;
1641
- if (typeof prepared.paymentId !== "string" || typeof prepared.preparedMessageHash !== "string" || typeof prepared.intentJson?.serializedTransaction !== "string" || typeof prepared.intentJson?.signingIntent !== "object") {
1642
- throw new X402ClientError(
1643
- "invalid_prepare_response",
1644
- "Facilitator prepare response is missing required fields"
1645
- );
1646
- }
1647
- return prepared;
1648
- }
1649
- };
1774
+ ]
1775
+ });
1776
+ return (url, init) => wrapped(url, init);
1777
+ }
1650
1778
 
1651
- // ../../demo/mcp-server.ts
1652
- var TOOL_NAME = "fetch_with_subly_payment";
1653
- var DEFAULT_MAX_AMOUNT_RAW_USDC = 10000n;
1779
+ // src/mcp-server.ts
1654
1780
  var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1655
- 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);
1781
+ var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
1782
+ var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
1656
1783
  var keyPairSigner = await loadKeyPairSigner({
1657
1784
  base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1658
1785
  jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1659
1786
  label: "SUBLY_DEMO_AGENT_KEYPAIR"
1660
1787
  });
1788
+ var agentSecretKey = loadSecretKeyBytes({
1789
+ base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1790
+ jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1791
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
1792
+ });
1661
1793
  var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1662
- var rpc = createRpc(
1663
- process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
1664
- );
1665
- async function fetchBudget() {
1666
- try {
1667
- const url = `${facilitatorBaseUrl}/v1/wallets/${signer.walletAddress}/budget`;
1668
- const response = await fetch(url, {
1669
- headers: await walletAuthHeaders({ signer, method: "GET", url })
1670
- });
1671
- if (response.status !== 200) {
1672
- return null;
1673
- }
1674
- const body = await response.json();
1675
- if (body.budget === void 0) {
1676
- return null;
1677
- }
1678
- return {
1679
- positionValueUsdc: formatRawUsdcAmount(body.budget.positionValueRawUsdc),
1680
- spendableYieldUsdc: formatRawUsdcAmount(
1681
- body.budget.spendableYieldRawUsdc
1682
- )
1683
- };
1684
- } catch {
1685
- return null;
1686
- }
1687
- }
1688
- async function paymentStatusFor(paymentId) {
1689
- try {
1690
- const url = `${facilitatorBaseUrl}/v1/payments/${paymentId}`;
1691
- const response = await fetch(url, {
1692
- headers: await walletAuthHeaders({ signer, method: "GET", url })
1693
- });
1694
- if (response.status !== 200) {
1695
- return "indeterminate";
1696
- }
1697
- const body = await response.json();
1698
- if (body.status === "settled") {
1699
- return "settled";
1700
- }
1701
- if (body.status === "expired" || body.status === "failed" || body.status === "failed_not_submitted") {
1702
- return "not_settled";
1703
- }
1704
- return "indeterminate";
1705
- } catch {
1706
- return "indeterminate";
1707
- }
1708
- }
1709
- function fileStateStore(path) {
1710
- return {
1711
- load() {
1712
- try {
1713
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
1714
- return Array.isArray(parsed) ? parsed : [];
1715
- } catch {
1716
- return [];
1717
- }
1718
- },
1719
- save(records) {
1720
- try {
1721
- writeFileSync(path, JSON.stringify(records));
1722
- } catch (error) {
1723
- console.error(
1724
- `[subly-mcp] failed to persist pending payments: ${error instanceof Error ? error.message : String(error)}`
1725
- );
1726
- }
1727
- }
1728
- };
1729
- }
1730
- var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? "demo/env/mcp-pending-payments.json";
1731
- var paidFetchService = new PaidFetchService({
1732
- signatureBuilder: new SublyX402Client({
1733
- facilitatorBaseUrl,
1734
- signer,
1735
- lookupTablesFor: (serializedTransaction) => fetchLookupTablesForTransaction(rpc, serializedTransaction)
1736
- }),
1737
- defaultMaxAmountRawUsdc,
1738
- fetchBudget,
1739
- paymentStatusFor,
1740
- stateStore: fileStateStore(pendingStatePath)
1794
+ var rpc = createRpc(rpcUrl);
1795
+ var payer = createRelayerX402Payer({
1796
+ facilitatorBaseUrl,
1797
+ signer,
1798
+ rpc,
1799
+ x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
1800
+ defaultMaxAmountRawUsdc
1741
1801
  });
1742
- 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.
1743
-
1744
- Before payments can succeed the operator of this server must have, once:
1745
- 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.
1746
- 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.
1747
- 3. Waited for yield to accrue; a payment needs the price plus a fixed overhead (~0.0024 USDC) of spendable yield.
1748
-
1749
- 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).`;
1750
- var server = new Server(
1751
- { name: "subly-payments", version: "0.1.1" },
1752
- { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1753
- );
1754
- server.setRequestHandler(ListToolsRequestSchema, () => ({
1755
- tools: [
1756
- {
1757
- name: TOOL_NAME,
1758
- 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(
1759
- defaultMaxAmountRawUsdc
1760
- )} 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.`,
1761
- inputSchema: {
1762
- type: "object",
1763
- properties: {
1764
- url: {
1765
- type: "string",
1766
- description: "URL to fetch (GET). Must match the seller's resource URL exactly."
1767
- },
1768
- maxAmountRawUsdc: {
1769
- type: "string",
1770
- 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.'
1771
- },
1772
- forceNewPayment: {
1773
- type: "boolean",
1774
- 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)."
1775
- }
1776
- },
1777
- required: ["url"]
1778
- },
1779
- annotations: {
1780
- title: "Fetch with Subly payment",
1781
- readOnlyHint: false,
1782
- destructiveHint: true,
1783
- idempotentHint: false,
1784
- openWorldHint: true
1785
- }
1786
- }
1787
- ]
1788
- }));
1789
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1790
- if (request.params.name !== TOOL_NAME) {
1791
- return {
1792
- content: [
1793
- { type: "text", text: `unknown tool: ${request.params.name}` }
1794
- ],
1795
- isError: true
1796
- };
1797
- }
1798
- const args = request.params.arguments ?? {};
1799
- const url = args.url;
1800
- if (typeof url !== "string" || url.length === 0) {
1801
- return {
1802
- content: [{ type: "text", text: "missing required argument: url" }],
1803
- isError: true
1804
- };
1805
- }
1806
- let maxAmountRawUsdc;
1807
- if (args.maxAmountRawUsdc !== void 0) {
1808
- const raw = args.maxAmountRawUsdc;
1809
- try {
1810
- if (typeof raw !== "string" && typeof raw !== "number") {
1811
- throw new TypeError("not a string or number");
1812
- }
1813
- maxAmountRawUsdc = BigInt(raw);
1814
- } catch {
1815
- return {
1816
- content: [
1817
- {
1818
- type: "text",
1819
- text: "maxAmountRawUsdc must be an integer raw USDC amount"
1820
- }
1821
- ],
1822
- isError: true
1823
- };
1824
- }
1825
- }
1826
- try {
1827
- const result = await paidFetchService.paidFetch({
1828
- url,
1829
- ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
1830
- forceNewPayment: args.forceNewPayment === true
1831
- });
1832
- return {
1833
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1834
- };
1835
- } catch (error) {
1836
- if (error instanceof PaidFetchError) {
1837
- return {
1838
- content: [
1839
- {
1840
- type: "text",
1841
- text: JSON.stringify(
1842
- {
1843
- paid: false,
1844
- refused: true,
1845
- reason: error.reason,
1846
- message: error.message,
1847
- detail: error.detail
1848
- },
1849
- null,
1850
- 2
1851
- )
1852
- }
1853
- ],
1854
- isError: true
1855
- };
1856
- }
1857
- if (error instanceof X402ClientError) {
1858
- return {
1859
- content: [
1860
- {
1861
- type: "text",
1862
- text: JSON.stringify(
1863
- {
1864
- paid: false,
1865
- refused: true,
1866
- reason: error.reason,
1867
- detail: error.detail ?? null
1868
- },
1869
- null,
1870
- 2
1871
- )
1872
- }
1873
- ],
1874
- isError: true
1875
- };
1876
- }
1877
- return {
1878
- content: [
1879
- {
1880
- type: "text",
1881
- text: error instanceof Error ? error.message : String(error)
1882
- }
1883
- ],
1884
- isError: true
1885
- };
1886
- }
1802
+ await runMcpPaymentServer({
1803
+ payer,
1804
+ signer,
1805
+ facilitatorBaseUrl,
1806
+ defaultMaxAmountRawUsdc
1887
1807
  });
1888
- try {
1889
- await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
1890
- console.error("[subly-mcp] wallet registered and synced at the facilitator");
1891
- } catch (error) {
1892
- console.error(
1893
- `[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
1894
- );
1895
- }
1896
- var transport = new StdioServerTransport();
1897
- await server.connect(transport);
1898
- console.error(
1899
- `[subly-mcp] ready: agent wallet ${signer.walletAddress}, facilitator ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc)} USDC`
1900
- );