@subly_fi/pay 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pay.js CHANGED
@@ -1,11 +1,12 @@
1
- // demo/pay.ts
2
- import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
1
+ // src/pay.ts
2
+ import { homedir } from "node:os";
3
+ import { join as join2 } from "node:path";
3
4
 
4
- // src/client/agent-wallet-signer.ts
5
+ // ../../src/client/agent-wallet-signer.ts
5
6
  import { signBytes } from "@solana/kit";
6
7
  import bs584 from "bs58";
7
8
 
8
- // src/solana/tx.ts
9
+ // ../../src/solana/tx.ts
9
10
  import bs58 from "bs58";
10
11
  import {
11
12
  appendTransactionMessageInstructions,
@@ -20,7 +21,7 @@ import {
20
21
  setTransactionMessageLifetimeUsingBlockhash
21
22
  } from "@solana/kit";
22
23
 
23
- // src/lib/hash.ts
24
+ // ../../src/lib/hash.ts
24
25
  import { createHash } from "node:crypto";
25
26
  var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
26
27
  function sha256TaggedHex(data) {
@@ -46,7 +47,7 @@ function hashStableJson(value) {
46
47
  return sha256TaggedHex(stableStringify(value));
47
48
  }
48
49
 
49
- // src/solana/tx.ts
50
+ // ../../src/solana/tx.ts
50
51
  function decodeSerializedTransaction(serializedBase64) {
51
52
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
52
53
  }
@@ -66,11 +67,11 @@ function signatureBase58ForSigner(transaction, signer2) {
66
67
  return bs58.encode(signature);
67
68
  }
68
69
 
69
- // src/client/transaction-intent-validator.ts
70
+ // ../../src/client/transaction-intent-validator.ts
70
71
  import bs583 from "bs58";
71
72
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
72
73
 
73
- // src/config/constants.ts
74
+ // ../../src/config/constants.ts
74
75
  var PAYMENT_SCHEME = "subly-yield-exact";
75
76
  var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
76
77
  var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
@@ -86,7 +87,7 @@ var SUBLY_VAULT = {
86
87
  };
87
88
  var USDC_DECIMALS = 6;
88
89
 
89
- // src/domain/request-binding.ts
90
+ // ../../src/domain/request-binding.ts
90
91
  function computeRequestBindingHash(fields) {
91
92
  return hashStableJson({
92
93
  sellerRequestId: fields.sellerRequestId,
@@ -101,7 +102,7 @@ function computeRequestBindingHash(fields) {
101
102
  });
102
103
  }
103
104
 
104
- // src/lib/associated-token-account.ts
105
+ // ../../src/lib/associated-token-account.ts
105
106
  import { createHash as createHash2 } from "node:crypto";
106
107
  import bs582 from "bs58";
107
108
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
@@ -193,7 +194,7 @@ function modPow(base, exponent, modulus) {
193
194
  return result;
194
195
  }
195
196
 
196
- // src/client/transaction-intent-validator.ts
197
+ // ../../src/client/transaction-intent-validator.ts
197
198
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
198
199
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
199
200
  var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
@@ -879,7 +880,7 @@ function readShortVec(bytes, startOffset) {
879
880
  return null;
880
881
  }
881
882
 
882
- // src/client/agent-wallet-signer.ts
883
+ // ../../src/client/agent-wallet-signer.ts
883
884
  var LocalKeypairAgentWalletSigner = class {
884
885
  validationMode = "structured_intent_transaction";
885
886
  keyPairSigner;
@@ -949,47 +950,7 @@ var LocalKeypairAgentWalletSigner = class {
949
950
  }
950
951
  };
951
952
 
952
- // src/client/lookup-tables.ts
953
- import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
954
- import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
955
- function lookupTableAddressesForTransaction(serializedTransaction) {
956
- const wire = Buffer.from(serializedTransaction, "base64");
957
- let offset = 0;
958
- let signatureCount = 0;
959
- let shift = 0;
960
- while (offset < wire.length) {
961
- const byte = wire[offset];
962
- signatureCount |= (byte & 127) << shift;
963
- offset += 1;
964
- if ((byte & 128) === 0) {
965
- break;
966
- }
967
- shift += 7;
968
- }
969
- const messageBytes = wire.subarray(offset + signatureCount * 64);
970
- const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
971
- const lookups = compiled.addressTableLookups ?? [];
972
- return lookups.map((lookup) => String(lookup.lookupTableAddress));
973
- }
974
- async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
975
- const addresses = lookupTableAddressesForTransaction(serializedTransaction);
976
- if (addresses.length === 0) {
977
- return {};
978
- }
979
- const tables = await fetchAllMaybeAddressLookupTable(
980
- rpc2,
981
- addresses.map((value) => address(value))
982
- );
983
- const result = {};
984
- for (const table of tables) {
985
- if (table.exists) {
986
- result[table.address] = table.data.addresses.map(String);
987
- }
988
- }
989
- return result;
990
- }
991
-
992
- // src/api/wallet-auth.ts
953
+ // ../../src/api/wallet-auth.ts
993
954
  import { createHash as createHash3 } from "node:crypto";
994
955
  import bs585 from "bs58";
995
956
  import nacl from "tweetnacl";
@@ -1007,7 +968,7 @@ function walletAuthMessage(params) {
1007
968
  );
1008
969
  }
1009
970
 
1010
- // src/client/wallet-auth-headers.ts
971
+ // ../../src/client/wallet-auth-headers.ts
1011
972
  async function walletAuthHeaders(params) {
1012
973
  const signedAtMs = String(Date.now());
1013
974
  const message = walletAuthMessage({
@@ -1023,7 +984,7 @@ async function walletAuthHeaders(params) {
1023
984
  };
1024
985
  }
1025
986
 
1026
- // src/client/onboarding.ts
987
+ // ../../src/client/onboarding.ts
1027
988
  var SELF_SERVE_POLICY_ID = "self-serve";
1028
989
  var OnboardingError = class extends Error {
1029
990
  constructor(step, message, detail = null) {
@@ -1038,9 +999,9 @@ var OnboardingError = class extends Error {
1038
999
  async function ensureWalletOnboarded(params) {
1039
1000
  const fetchImpl = params.fetchImpl ?? fetch;
1040
1001
  const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
1041
- const post = async (step, path, body) => {
1002
+ const post = async (step, path, body2) => {
1042
1003
  const url2 = `${baseUrl}${path}`;
1043
- const serialized = JSON.stringify(body);
1004
+ const serialized = JSON.stringify(body2);
1044
1005
  const response = await fetchImpl(url2, {
1045
1006
  method: "POST",
1046
1007
  headers: {
@@ -1080,12 +1041,375 @@ async function ensureWalletOnboarded(params) {
1080
1041
  await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1081
1042
  }
1082
1043
 
1083
- // src/x402/headers.ts
1044
+ // ../../src/client/lookup-tables.ts
1045
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1046
+ import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
1047
+ function lookupTableAddressesForTransaction(serializedTransaction) {
1048
+ const wire = Buffer.from(serializedTransaction, "base64");
1049
+ let offset = 0;
1050
+ let signatureCount = 0;
1051
+ let shift = 0;
1052
+ while (offset < wire.length) {
1053
+ const byte = wire[offset];
1054
+ signatureCount |= (byte & 127) << shift;
1055
+ offset += 1;
1056
+ if ((byte & 128) === 0) {
1057
+ break;
1058
+ }
1059
+ shift += 7;
1060
+ }
1061
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
1062
+ const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
1063
+ const lookups = compiled.addressTableLookups ?? [];
1064
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
1065
+ }
1066
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1067
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1068
+ if (addresses.length === 0) {
1069
+ return {};
1070
+ }
1071
+ const tables = await fetchAllMaybeAddressLookupTable(
1072
+ rpc2,
1073
+ addresses.map((value) => address(value))
1074
+ );
1075
+ const result = {};
1076
+ for (const table of tables) {
1077
+ if (table.exists) {
1078
+ result[table.address] = table.data.addresses.map(String);
1079
+ }
1080
+ }
1081
+ return result;
1082
+ }
1083
+
1084
+ // ../../src/client/vault-flows.ts
1085
+ var VaultFlowClientError = class extends Error {
1086
+ constructor(step, message, detail = null) {
1087
+ super(message);
1088
+ this.step = step;
1089
+ this.detail = detail;
1090
+ this.name = "VaultFlowClientError";
1091
+ }
1092
+ step;
1093
+ detail;
1094
+ };
1095
+ var VaultFlowClient = class {
1096
+ baseUrl;
1097
+ signer;
1098
+ fetchImpl;
1099
+ lookupTablesFor;
1100
+ pollTimeoutMs;
1101
+ pollIntervalMs;
1102
+ constructor(config) {
1103
+ this.baseUrl = config.facilitatorBaseUrl.replace(/\/$/, "");
1104
+ this.signer = config.signer;
1105
+ this.fetchImpl = config.fetchImpl ?? fetch;
1106
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1107
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1108
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1109
+ }
1110
+ /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1111
+ async deposit(input) {
1112
+ const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1113
+ wallet: this.signer.walletAddress,
1114
+ amountRawUsdc: input.amountRawUsdc.toString()
1115
+ });
1116
+ const signed = await this.signer.signDeposit({
1117
+ intent: prepared.signingIntent,
1118
+ serializedTransaction: prepared.serializedTransaction,
1119
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1120
+ });
1121
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
1122
+ depositId: prepared.depositId,
1123
+ serializedTransaction: signed.serializedTransaction,
1124
+ agentSignature: signed.agentSignature
1125
+ });
1126
+ if (outcome.status === "submitted") {
1127
+ outcome = await this.pollUntilTerminal(
1128
+ `/v1/deposits/${prepared.depositId}`,
1129
+ outcome
1130
+ );
1131
+ }
1132
+ return {
1133
+ depositId: prepared.depositId,
1134
+ status: outcome.status,
1135
+ txSignature: outcome.txSignature ?? null,
1136
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
1137
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
1138
+ errorCode: outcome.errorCode ?? null
1139
+ };
1140
+ }
1141
+ /**
1142
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
1143
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
1144
+ * with purpose "yield_realize" the relayer refuses anything beyond the
1145
+ * spendable yield (the payment path, via RelayerYieldRealizer).
1146
+ */
1147
+ async withdraw(input) {
1148
+ const prepared = await this.postJson(
1149
+ "prepare",
1150
+ "/v1/withdrawals/prepare",
1151
+ {
1152
+ wallet: this.signer.walletAddress,
1153
+ amountRawUsdc: input.amountRawUsdc.toString(),
1154
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1155
+ }
1156
+ );
1157
+ const signed = await this.signer.signWithdrawal({
1158
+ intent: prepared.signingIntent,
1159
+ serializedTransaction: prepared.serializedTransaction,
1160
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1161
+ });
1162
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
1163
+ withdrawalId: prepared.withdrawalId,
1164
+ serializedTransaction: signed.serializedTransaction,
1165
+ agentSignature: signed.agentSignature
1166
+ });
1167
+ if (outcome.status === "submitted") {
1168
+ outcome = await this.pollUntilTerminal(
1169
+ `/v1/withdrawals/${prepared.withdrawalId}`,
1170
+ outcome
1171
+ );
1172
+ }
1173
+ return {
1174
+ withdrawalId: prepared.withdrawalId,
1175
+ status: outcome.status,
1176
+ txSignature: outcome.txSignature ?? null,
1177
+ destinationUsdcAta: prepared.destinationUsdcAta,
1178
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
1179
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
1180
+ errorCode: outcome.errorCode ?? null
1181
+ };
1182
+ }
1183
+ /**
1184
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
1185
+ * yield accrued since the last sync shows up); the sync is best-effort and
1186
+ * on failure the last-synced view is returned.
1187
+ */
1188
+ async getBudget(options = {}) {
1189
+ if (options.refreshFromChain !== false) {
1190
+ try {
1191
+ await this.postJson(
1192
+ "sync",
1193
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
1194
+ { source: "chain" }
1195
+ );
1196
+ } catch {
1197
+ }
1198
+ }
1199
+ const url2 = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1200
+ const response = await this.fetchImpl(url2, {
1201
+ headers: await walletAuthHeaders({
1202
+ signer: this.signer,
1203
+ method: "GET",
1204
+ url: url2
1205
+ })
1206
+ });
1207
+ const text = await response.text();
1208
+ if (response.status !== 200) {
1209
+ throw new VaultFlowClientError(
1210
+ "budget",
1211
+ `budget endpoint returned ${response.status}: ${text}`
1212
+ );
1213
+ }
1214
+ let parsed;
1215
+ try {
1216
+ parsed = JSON.parse(text);
1217
+ } catch {
1218
+ throw new VaultFlowClientError(
1219
+ "budget",
1220
+ "budget endpoint returned 200 with a non-JSON body",
1221
+ text
1222
+ );
1223
+ }
1224
+ const body2 = parsed;
1225
+ return {
1226
+ wallet: this.signer.walletAddress,
1227
+ principalBasisRawUsdc: body2.position?.principalBasisRawUsdc ?? "0",
1228
+ positionValueRawUsdc: body2.budget?.positionValueRawUsdc ?? "0",
1229
+ grossYieldRawUsdc: body2.budget?.grossYieldRawUsdc ?? "0",
1230
+ spendableYieldRawUsdc: body2.budget?.spendableYieldRawUsdc ?? "0"
1231
+ };
1232
+ }
1233
+ /**
1234
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
1235
+ * (each read looks the tx up on-chain) or the timeout elapses.
1236
+ */
1237
+ async pollUntilTerminal(path, last) {
1238
+ const deadline = Date.now() + this.pollTimeoutMs;
1239
+ let latest = last;
1240
+ while (Date.now() < deadline) {
1241
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1242
+ const url2 = `${this.baseUrl}${path}`;
1243
+ const response = await this.fetchImpl(url2, {
1244
+ headers: await walletAuthHeaders({
1245
+ signer: this.signer,
1246
+ method: "GET",
1247
+ url: url2
1248
+ })
1249
+ });
1250
+ if (response.status !== 200) {
1251
+ continue;
1252
+ }
1253
+ try {
1254
+ latest = await response.json();
1255
+ } catch {
1256
+ continue;
1257
+ }
1258
+ if (latest.status !== "submitted") {
1259
+ return latest;
1260
+ }
1261
+ }
1262
+ return latest;
1263
+ }
1264
+ async postJson(step, path, body2) {
1265
+ const url2 = `${this.baseUrl}${path}`;
1266
+ const serialized = JSON.stringify(body2);
1267
+ const response = await this.fetchImpl(url2, {
1268
+ method: "POST",
1269
+ headers: {
1270
+ ...await walletAuthHeaders({
1271
+ signer: this.signer,
1272
+ method: "POST",
1273
+ url: url2,
1274
+ body: serialized
1275
+ }),
1276
+ "content-type": "application/json"
1277
+ },
1278
+ body: serialized
1279
+ });
1280
+ const text = await response.text();
1281
+ if (response.status !== 200) {
1282
+ throw new VaultFlowClientError(
1283
+ step,
1284
+ `${path} failed with ${response.status}: ${text}`,
1285
+ text
1286
+ );
1287
+ }
1288
+ try {
1289
+ return JSON.parse(text);
1290
+ } catch {
1291
+ throw new VaultFlowClientError(
1292
+ step,
1293
+ `${path} returned 200 with a non-JSON body`,
1294
+ text
1295
+ );
1296
+ }
1297
+ }
1298
+ };
1299
+
1300
+ // ../../src/client/relayer-yield-realizer.ts
1301
+ var REALIZE_OVERHEAD_RAW_USDC = 2500n;
1302
+ var RelayerRealizeError = class extends Error {
1303
+ constructor(code, message, detail = null) {
1304
+ super(message);
1305
+ this.code = code;
1306
+ this.detail = detail;
1307
+ this.name = "RelayerRealizeError";
1308
+ }
1309
+ code;
1310
+ detail;
1311
+ };
1312
+ var RelayerYieldRealizer = class {
1313
+ vaultFlows;
1314
+ constructor(config) {
1315
+ this.vaultFlows = new VaultFlowClient({
1316
+ facilitatorBaseUrl: config.facilitatorBaseUrl,
1317
+ signer: config.signer,
1318
+ rpc: config.rpc,
1319
+ ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
1320
+ ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
1321
+ });
1322
+ }
1323
+ async ensureUsdcAvailable(input) {
1324
+ const shortfallRawUsdc = input.amountRawUsdc;
1325
+ await this.assertSpendableYield(shortfallRawUsdc);
1326
+ let outcome;
1327
+ try {
1328
+ outcome = await this.vaultFlows.withdraw({
1329
+ amountRawUsdc: shortfallRawUsdc,
1330
+ // The relayer refuses to prepare this withdrawal beyond the spendable
1331
+ // yield — the principal-protection guard the client cannot bypass.
1332
+ purpose: "yield_realize"
1333
+ });
1334
+ } catch (error) {
1335
+ throw this.mapWithdrawError(error);
1336
+ }
1337
+ if (outcome.status !== "confirmed" || outcome.txSignature === null) {
1338
+ throw new RelayerRealizeError(
1339
+ "realize_not_confirmed",
1340
+ `yield realize withdrawal did not confirm (status=${outcome.status})`,
1341
+ outcome
1342
+ );
1343
+ }
1344
+ return {
1345
+ realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
1346
+ txSignature: outcome.txSignature
1347
+ };
1348
+ }
1349
+ /**
1350
+ * Refuses to realize more than the ledger's spendable yield (principal).
1351
+ * getBudget syncs the relayer's ledger from chain first (best-effort), so a
1352
+ * long-running client sees yield as it accrues instead of a frozen view.
1353
+ */
1354
+ async assertSpendableYield(shortfallRawUsdc) {
1355
+ let spendable;
1356
+ try {
1357
+ const budget = await this.vaultFlows.getBudget();
1358
+ spendable = BigInt(budget.spendableYieldRawUsdc);
1359
+ } catch (error) {
1360
+ throw new RelayerRealizeError(
1361
+ "budget_unavailable",
1362
+ "could not read the spendable-yield budget",
1363
+ error
1364
+ );
1365
+ }
1366
+ const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
1367
+ if (spendable < requiredRawUsdc) {
1368
+ throw new RelayerRealizeError(
1369
+ "insufficient_yield",
1370
+ `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
1371
+ { spendableYieldRawUsdc: spendable.toString() }
1372
+ );
1373
+ }
1374
+ }
1375
+ mapWithdrawError(error) {
1376
+ if (!(error instanceof VaultFlowClientError)) {
1377
+ return new RelayerRealizeError(
1378
+ "prepare_failed",
1379
+ `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
1380
+ error
1381
+ );
1382
+ }
1383
+ const serverCode = errorCodeFrom(error.detail);
1384
+ if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
1385
+ return new RelayerRealizeError(
1386
+ "insufficient_yield",
1387
+ "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
1388
+ error.detail
1389
+ );
1390
+ }
1391
+ return new RelayerRealizeError(
1392
+ error.step === "submit" ? "submit_failed" : "prepare_failed",
1393
+ error.message,
1394
+ error.detail
1395
+ );
1396
+ }
1397
+ };
1398
+ function errorCodeFrom(detail) {
1399
+ if (typeof detail !== "string") {
1400
+ return null;
1401
+ }
1402
+ try {
1403
+ const parsed = JSON.parse(detail);
1404
+ return typeof parsed.error?.code === "string" ? parsed.error.code : null;
1405
+ } catch {
1406
+ return null;
1407
+ }
1408
+ }
1409
+
1410
+ // ../../src/x402/headers.ts
1084
1411
  import { z } from "zod";
1085
1412
  var PAYMENT_REQUIRED_HEADER = "payment-required";
1086
- var PAYMENT_SIGNATURE_HEADER = "payment-signature";
1087
- var PAYMENT_RESPONSE_HEADER = "payment-response";
1088
- var X402_VERSION = 2;
1089
1413
  var MAX_HEADER_JSON_BYTES = 16384;
1090
1414
  var X402HeaderError = class extends Error {
1091
1415
  reason;
@@ -1132,16 +1456,6 @@ var sublyPaymentPayloadSchema = z.object({
1132
1456
  temporarySettlementSignature: z.string().min(1).max(128)
1133
1457
  })
1134
1458
  }).loose();
1135
- function encodeX402Header(value) {
1136
- const json = JSON.stringify(value);
1137
- if (Buffer.byteLength(json, "utf8") > MAX_HEADER_JSON_BYTES) {
1138
- throw new X402HeaderError(
1139
- "header_too_large",
1140
- `x402 header JSON exceeds ${MAX_HEADER_JSON_BYTES} bytes`
1141
- );
1142
- }
1143
- return Buffer.from(json, "utf8").toString("base64");
1144
- }
1145
1459
  function decodeX402Header(headerValue) {
1146
1460
  if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
1147
1461
  throw new X402HeaderError(
@@ -1159,285 +1473,466 @@ function decodeX402Header(headerValue) {
1159
1473
  );
1160
1474
  }
1161
1475
  }
1162
- function decodePaymentRequiredHeader(headerValue) {
1163
- const parsed = paymentRequiredSchema.safeParse(decodeX402Header(headerValue));
1476
+ function requestBodyHashFor(body2) {
1477
+ if (body2 === null || body2 === void 0 || body2.length === 0) {
1478
+ return EMPTY_BODY_HASH;
1479
+ }
1480
+ return sha256TaggedHex(
1481
+ typeof body2 === "string" ? Buffer.from(body2, "utf8") : Buffer.from(body2)
1482
+ );
1483
+ }
1484
+
1485
+ // ../../src/x402/standard-requirements.ts
1486
+ import { z as z2 } from "zod";
1487
+ var STANDARD_EXACT_SCHEME = "exact";
1488
+ var standardExactRequirementSchema = z2.object({
1489
+ scheme: z2.literal(STANDARD_EXACT_SCHEME),
1490
+ /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
1491
+ network: z2.string().min(1),
1492
+ /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
1493
+ asset: z2.string().min(1),
1494
+ /** Exact price in the asset's atomic units, as a decimal string. */
1495
+ amount: z2.string().regex(/^[1-9]\d*$/),
1496
+ /** Recipient wallet; the transfer destination ATA is derived from it. */
1497
+ payTo: z2.string().min(1),
1498
+ maxTimeoutSeconds: z2.number().int().positive().optional(),
1499
+ extra: z2.object({
1500
+ /** Facilitator address that pays the tx fee (gas sponsorship). */
1501
+ feePayer: z2.string().min(1).optional()
1502
+ }).loose().optional()
1503
+ }).loose();
1504
+ var standardPaymentRequiredSchema = z2.object({
1505
+ x402Version: z2.number().int(),
1506
+ accepts: z2.array(z2.unknown()),
1507
+ error: z2.string().optional(),
1508
+ resource: z2.object({ url: z2.string().optional() }).loose().optional()
1509
+ }).loose();
1510
+ var StandardX402ChallengeError = class extends Error {
1511
+ reason;
1512
+ constructor(reason, message) {
1513
+ super(message);
1514
+ this.name = "StandardX402ChallengeError";
1515
+ this.reason = reason;
1516
+ }
1517
+ };
1518
+ function parseStandardChallenge(challenge) {
1519
+ const parsed = standardPaymentRequiredSchema.safeParse(challenge);
1164
1520
  if (!parsed.success) {
1165
- throw new X402HeaderError(
1521
+ throw new StandardX402ChallengeError(
1166
1522
  "invalid_payment_required",
1167
- "PAYMENT-REQUIRED header is not a valid x402 PaymentRequired object"
1523
+ "Response is not a valid x402 PaymentRequired object"
1168
1524
  );
1169
1525
  }
1170
- const sublyRequirements = parsed.data.accepts.flatMap((candidate) => {
1171
- const requirement = sublyPaymentRequirementsSchema.safeParse(candidate);
1172
- return requirement.success ? [requirement.data] : [];
1526
+ const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
1527
+ const requirement = standardExactRequirementSchema.safeParse(candidate);
1528
+ if (!requirement.success) {
1529
+ return [];
1530
+ }
1531
+ return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
1173
1532
  });
1174
- return { paymentRequired: parsed.data, sublyRequirements };
1533
+ return { paymentRequired: parsed.data, solanaExactRequirements };
1175
1534
  }
1176
- function requestBodyHashFor(body) {
1177
- if (body === null || body === void 0 || body.length === 0) {
1178
- return EMPTY_BODY_HASH;
1535
+ function decodeStandardPaymentRequiredHeader(headerValue) {
1536
+ let decoded;
1537
+ try {
1538
+ decoded = decodeX402Header(headerValue);
1539
+ } catch (error) {
1540
+ throw new StandardX402ChallengeError(
1541
+ error instanceof X402HeaderError ? error.reason : "invalid_header",
1542
+ "Cannot decode the payment-required header"
1543
+ );
1179
1544
  }
1180
- return sha256TaggedHex(
1181
- typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1182
- );
1545
+ return parseStandardChallenge(decoded);
1546
+ }
1547
+ function selectPayableSolanaRequirement(requirements, options) {
1548
+ const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1549
+ const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1550
+ const requirement = requirements.find(
1551
+ (candidate) => candidate.network === network && candidate.asset === usdcMint
1552
+ ) ?? null;
1553
+ if (requirement === null) {
1554
+ throw new StandardX402ChallengeError(
1555
+ "no_payable_requirement",
1556
+ `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1557
+ );
1558
+ }
1559
+ return {
1560
+ requirement,
1561
+ amountRawUsdc: BigInt(requirement.amount),
1562
+ payTo: requirement.payTo,
1563
+ feePayer: requirement.extra?.feePayer ?? null
1564
+ };
1565
+ }
1566
+ function standardRequirementMatchesSelected(candidate, selected) {
1567
+ const parsed = standardExactRequirementSchema.safeParse(candidate);
1568
+ return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
1569
+ }
1570
+ function stableJson(value) {
1571
+ return JSON.stringify(sortJson(value));
1572
+ }
1573
+ function sortJson(value) {
1574
+ if (Array.isArray(value)) {
1575
+ return value.map(sortJson);
1576
+ }
1577
+ if (value !== null && typeof value === "object") {
1578
+ return Object.fromEntries(
1579
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
1580
+ );
1581
+ }
1582
+ return value;
1183
1583
  }
1184
1584
 
1185
- // src/client/paid-fetch.ts
1186
- var PaidFetchError = class extends Error {
1585
+ // ../../src/client/standard-x402-payer.ts
1586
+ var StandardX402PayError = class extends Error {
1187
1587
  constructor(reason, message, detail = null) {
1188
1588
  super(message);
1189
1589
  this.reason = reason;
1190
1590
  this.detail = detail;
1191
- this.name = "PaidFetchError";
1591
+ this.name = "StandardX402PayError";
1192
1592
  }
1193
1593
  reason;
1194
1594
  detail;
1195
1595
  };
1196
- var DEFAULT_PENDING_TTL_MS = 11e4;
1197
- var DEFAULT_MAX_TRACKED_URLS = 1e3;
1198
- var DEFAULT_MAX_BODY_CHARS = 2e4;
1199
- function formatRawUsdcAmount(raw) {
1200
- const value = BigInt(raw);
1201
- const negative = value < 0n;
1202
- const abs = negative ? -value : value;
1203
- const whole = abs / 1000000n;
1204
- const frac = (abs % 1000000n).toString().padStart(6, "0");
1205
- return `${negative ? "-" : ""}${whole}.${frac}`;
1206
- }
1207
- var PaidFetchService = class {
1208
- signatureBuilder;
1209
- fetchImpl;
1210
- fetchBudget;
1211
- paymentStatusFor;
1596
+ var StandardX402Payer = class {
1597
+ realizer;
1598
+ x402Fetch;
1599
+ probeFetch;
1212
1600
  defaultMaxAmountRawUsdc;
1213
- pendingTtlMs;
1214
- maxTrackedUrls;
1215
- maxBodyChars;
1216
- nowMs;
1601
+ network;
1602
+ usdcMint;
1217
1603
  stateStore;
1218
1604
  pending = /* @__PURE__ */ new Map();
1219
1605
  inFlight = /* @__PURE__ */ new Map();
1606
+ nowMs;
1220
1607
  constructor(config) {
1221
- this.signatureBuilder = config.signatureBuilder;
1222
- this.fetchImpl = config.fetchImpl ?? fetch;
1223
- this.fetchBudget = config.fetchBudget ?? (async () => null);
1224
- this.paymentStatusFor = config.paymentStatusFor ?? (async () => "indeterminate");
1608
+ this.realizer = config.realizer;
1609
+ this.x402Fetch = config.x402Fetch;
1610
+ this.probeFetch = config.probeFetch ?? fetch;
1225
1611
  this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1226
- this.pendingTtlMs = config.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
1227
- this.maxTrackedUrls = config.maxTrackedUrls ?? DEFAULT_MAX_TRACKED_URLS;
1228
- this.maxBodyChars = config.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS;
1229
- this.nowMs = config.nowMs ?? (() => Date.now());
1612
+ this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1613
+ this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1230
1614
  this.stateStore = config.stateStore ?? null;
1615
+ this.nowMs = config.nowMs ?? (() => Date.now());
1231
1616
  if (this.stateStore !== null) {
1232
1617
  for (const record of this.stateStore.load()) {
1233
- const { url: url2, ...entry } = record;
1234
- this.pending.set(url2, entry);
1618
+ this.pending.set(record.key, record);
1235
1619
  }
1236
1620
  }
1237
1621
  }
1238
- /**
1239
- * GET the URL, paying a 402 challenge when needed. Concurrent calls for the
1240
- * same URL share one flow and one result.
1241
- */
1242
- paidFetch(params) {
1243
- const existing = this.inFlight.get(params.url);
1244
- if (existing !== void 0) {
1245
- return existing;
1622
+ pay(input) {
1623
+ const method2 = (input.method ?? "GET").toUpperCase();
1624
+ const requestBodyHash = requestBodyHashFor(input.body ?? null);
1625
+ const pendingKey = pendingPaymentKey({
1626
+ url: input.url,
1627
+ method: method2,
1628
+ requestBodyHash
1629
+ });
1630
+ const existingFlow = this.inFlight.get(pendingKey);
1631
+ if (existingFlow !== void 0) {
1632
+ return existingFlow;
1246
1633
  }
1247
- const flow = this.run(params).finally(() => {
1248
- this.inFlight.delete(params.url);
1634
+ const flow = this.run(input, {
1635
+ method: method2,
1636
+ requestBodyHash,
1637
+ pendingKey
1638
+ }).finally(() => {
1639
+ this.inFlight.delete(pendingKey);
1249
1640
  });
1250
- this.inFlight.set(params.url, flow);
1641
+ this.inFlight.set(pendingKey, flow);
1251
1642
  return flow;
1252
1643
  }
1253
- async run(params) {
1254
- const { url: url2 } = params;
1255
- const pending = this.pending.get(url2);
1256
- if (pending !== void 0) {
1257
- const expired = this.nowMs() - pending.challengeAtMs > this.pendingTtlMs;
1258
- if (pending.unresolved || expired) {
1259
- if (params.forceNewPayment === true) {
1260
- this.untrack(url2);
1261
- } else {
1262
- const outcome = await this.paymentStatusFor(pending.paymentId);
1263
- if (outcome === "not_settled") {
1264
- this.untrack(url2);
1265
- } else if (outcome === "settled") {
1266
- throw new PaidFetchError(
1267
- "payment_already_settled",
1268
- `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.`,
1269
- { paymentId: pending.paymentId }
1270
- );
1271
- } else {
1272
- throw new PaidFetchError(
1273
- "payment_outcome_unknown",
1274
- `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.`,
1275
- { paymentId: pending.paymentId }
1276
- );
1277
- }
1278
- }
1279
- } else {
1280
- return this.deliver(url2, pending, {
1281
- retried: true,
1282
- budgetBefore: null
1283
- });
1644
+ async run(input, computed) {
1645
+ const { method: method2, requestBodyHash, pendingKey } = computed;
1646
+ const existingPending = this.pending.get(pendingKey);
1647
+ if (existingPending !== void 0 && input.forceNewPayment !== true) {
1648
+ throw new StandardX402PayError(
1649
+ "payment_outcome_unknown",
1650
+ "a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
1651
+ existingPending
1652
+ );
1653
+ }
1654
+ if (existingPending !== void 0 && input.forceNewPayment === true) {
1655
+ try {
1656
+ this.untrack(pendingKey);
1657
+ } catch (error) {
1658
+ throw new StandardX402PayError(
1659
+ "state_persist_failed",
1660
+ "could not clear the previous pending x402 marker before forcing a new payment",
1661
+ error
1662
+ );
1284
1663
  }
1285
1664
  }
1286
- const challengeAtMs = this.nowMs();
1287
- const first = await this.fetchImpl(url2);
1288
- const firstText = await first.text();
1289
- if (first.status !== 402) {
1290
- return {
1291
- paid: false,
1292
- status: first.status,
1293
- body: this.truncated(firstText)
1294
- };
1665
+ const init = {
1666
+ method: method2,
1667
+ ...input.body === void 0 ? {} : { body: input.body },
1668
+ ...input.headers === void 0 ? {} : { headers: input.headers }
1669
+ };
1670
+ const probe = await this.probeFetch(input.url, init);
1671
+ if (probe.status !== 402) {
1672
+ return { paid: false, status: probe.status, body: await probe.text() };
1295
1673
  }
1296
- const challengeHeader = first.headers.get(PAYMENT_REQUIRED_HEADER);
1297
- if (challengeHeader === null) {
1298
- throw new PaidFetchError(
1299
- "invalid_challenge",
1300
- "402 response is missing the PAYMENT-REQUIRED header"
1674
+ const selected = await this.selectRequirement(probe);
1675
+ const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1676
+ if (selected.amountRawUsdc > cap) {
1677
+ throw new StandardX402PayError(
1678
+ "amount_exceeds_client_cap",
1679
+ `the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
1680
+ { amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
1301
1681
  );
1302
1682
  }
1303
- const requirement = decodePaymentRequiredHeader(challengeHeader).sublyRequirements[0];
1304
- if (requirement === void 0) {
1305
- throw new PaidFetchError(
1306
- "invalid_challenge",
1307
- "402 challenge contains no subly-yield-exact requirement"
1683
+ let realized;
1684
+ try {
1685
+ realized = await this.realizer.ensureUsdcAvailable({
1686
+ amountRawUsdc: selected.amountRawUsdc
1687
+ });
1688
+ } catch (error) {
1689
+ throw new StandardX402PayError(
1690
+ "realize_failed",
1691
+ `could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
1692
+ error
1308
1693
  );
1309
1694
  }
1310
- const amountRawUsdc = BigInt(requirement.amountRawUsdc);
1311
- const maxAmountRawUsdc = params.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1312
- if (amountRawUsdc > maxAmountRawUsdc) {
1313
- throw new PaidFetchError(
1314
- "amount_exceeds_client_cap",
1315
- `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.`,
1316
- {
1317
- amountRawUsdc: requirement.amountRawUsdc,
1318
- maxAmountRawUsdc: maxAmountRawUsdc.toString(),
1319
- payTo: requirement.payTo
1320
- }
1695
+ const pendingRecord = {
1696
+ key: pendingKey,
1697
+ url: input.url,
1698
+ method: method2,
1699
+ requestBodyHash,
1700
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1701
+ payTo: selected.payTo,
1702
+ feePayer: selected.feePayer,
1703
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1704
+ realizeTxSignature: realized.txSignature,
1705
+ status: "realized",
1706
+ createdAtMs: this.nowMs(),
1707
+ updatedAtMs: this.nowMs()
1708
+ };
1709
+ try {
1710
+ this.track(pendingRecord);
1711
+ } catch (error) {
1712
+ throw new StandardX402PayError(
1713
+ "state_persist_failed",
1714
+ "could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
1715
+ { error, pendingPayment: pendingRecord }
1321
1716
  );
1322
1717
  }
1323
- const budgetBefore = await this.fetchBudget();
1324
- const { headerValue, paymentId } = await this.signatureBuilder.buildPaymentSignatureHeader({
1325
- paymentRequiredHeader: challengeHeader,
1326
- httpMethod: "GET",
1327
- url: url2
1328
- });
1329
- const entry = {
1330
- headerValue,
1331
- paymentId,
1332
- amountUsdc: formatRawUsdcAmount(amountRawUsdc),
1333
- payTo: requirement.payTo,
1334
- challengeAtMs,
1335
- unresolved: false
1718
+ let response;
1719
+ try {
1720
+ response = await this.x402Fetch(input.url, init, selected);
1721
+ } catch (error) {
1722
+ const persistError = this.tryMarkUnknown(pendingKey, {
1723
+ message: error instanceof Error ? error.message : String(error)
1724
+ });
1725
+ throw new StandardX402PayError(
1726
+ "payment_outcome_unknown",
1727
+ `the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
1728
+ { error, persistError }
1729
+ );
1730
+ }
1731
+ const bodyText = await response.text();
1732
+ if (response.status !== 200) {
1733
+ const persistError = this.tryMarkUnknown(pendingKey, {
1734
+ status: response.status,
1735
+ body: bodyText
1736
+ });
1737
+ throw new StandardX402PayError(
1738
+ "payment_outcome_unknown",
1739
+ `the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
1740
+ { status: response.status, body: bodyText, persistError }
1741
+ );
1742
+ }
1743
+ this.clearDelivered(pendingKey);
1744
+ return {
1745
+ paid: true,
1746
+ status: response.status,
1747
+ body: bodyText,
1748
+ payment: {
1749
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1750
+ payTo: selected.payTo,
1751
+ feePayer: selected.feePayer,
1752
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1753
+ realizeTxSignature: realized.txSignature
1754
+ }
1336
1755
  };
1337
- this.track(url2, entry);
1338
- return this.deliver(url2, entry, { retried: false, budgetBefore });
1339
1756
  }
1340
- /**
1341
- * Sends the signed PAYMENT-SIGNATURE retry. The tracked entry is removed
1342
- * only on confirmed delivery (200). A fresh 402 proves the signature can no
1343
- * longer settle, so the entry is kept as an unresolved marker; every other
1344
- * failure keeps it retryable so the next call reuses the same signature.
1345
- */
1346
- async deliver(url2, entry, params) {
1347
- let second;
1348
- let secondText;
1757
+ /** Reads the challenge from the header (preferred) or the JSON body. */
1758
+ async selectRequirement(probe) {
1759
+ const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
1760
+ let requirements;
1349
1761
  try {
1350
- second = await this.fetchImpl(url2, {
1351
- headers: { [PAYMENT_SIGNATURE_HEADER]: entry.headerValue }
1762
+ if (header !== null) {
1763
+ requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
1764
+ } else {
1765
+ requirements = parseStandardChallenge(
1766
+ await probe.json()
1767
+ ).solanaExactRequirements;
1768
+ }
1769
+ } catch (error) {
1770
+ throw new StandardX402PayError(
1771
+ error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
1772
+ "could not parse the x402 402 challenge",
1773
+ error
1774
+ );
1775
+ }
1776
+ try {
1777
+ return selectPayableSolanaRequirement(requirements, {
1778
+ network: this.network,
1779
+ usdcMint: this.usdcMint
1352
1780
  });
1353
- secondText = await second.text();
1354
1781
  } catch (error) {
1355
- throw new PaidFetchError(
1356
- "delivery_failed_payment_pending",
1357
- `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.`,
1358
- { paymentId: entry.paymentId }
1782
+ throw new StandardX402PayError(
1783
+ "no_payable_requirement",
1784
+ error instanceof Error ? error.message : String(error),
1785
+ error
1359
1786
  );
1360
1787
  }
1361
- if (second.status === 200) {
1362
- this.untrack(url2);
1363
- const transaction = receiptTransaction(second.headers);
1364
- return {
1365
- paid: true,
1366
- status: second.status,
1367
- body: this.truncated(secondText),
1368
- ...params.retried ? { retriedPendingPayment: true } : {},
1369
- payment: {
1370
- amountUsdc: entry.amountUsdc,
1371
- payTo: entry.payTo,
1372
- paymentId: entry.paymentId,
1373
- transaction,
1374
- solscanUrl: transaction === null ? null : `https://solscan.io/tx/${transaction}`,
1375
- budgetBefore: params.budgetBefore,
1376
- budgetAfter: await this.fetchBudget()
1377
- }
1378
- };
1788
+ }
1789
+ track(record) {
1790
+ const previous = this.pending.get(record.key);
1791
+ this.pending.set(record.key, record);
1792
+ try {
1793
+ this.persist();
1794
+ } catch (error) {
1795
+ if (previous === void 0) {
1796
+ this.pending.delete(record.key);
1797
+ } else {
1798
+ this.pending.set(record.key, previous);
1799
+ }
1800
+ throw error;
1801
+ }
1802
+ }
1803
+ markUnknown(key, detail) {
1804
+ const current = this.pending.get(key);
1805
+ if (current === void 0) {
1806
+ return;
1379
1807
  }
1380
- if (second.status === 402) {
1381
- entry.unresolved = true;
1808
+ const next = {
1809
+ ...current,
1810
+ status: "external_outcome_unknown",
1811
+ updatedAtMs: this.nowMs(),
1812
+ detail
1813
+ };
1814
+ this.pending.set(key, next);
1815
+ try {
1382
1816
  this.persist();
1383
- throw new PaidFetchError(
1384
- "payment_outcome_unknown",
1385
- `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.`,
1386
- { paymentId: entry.paymentId }
1387
- );
1817
+ } catch (error) {
1818
+ this.pending.set(key, current);
1819
+ throw error;
1820
+ }
1821
+ }
1822
+ tryMarkUnknown(key, detail) {
1823
+ try {
1824
+ this.markUnknown(key, detail);
1825
+ return null;
1826
+ } catch (error) {
1827
+ return error;
1388
1828
  }
1389
- throw new PaidFetchError(
1390
- "delivery_failed_payment_pending",
1391
- `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.`,
1392
- {
1393
- paymentId: entry.paymentId,
1394
- status: second.status,
1395
- body: this.truncated(secondText)
1396
- }
1397
- );
1398
1829
  }
1399
- track(url2, entry) {
1400
- if (!this.pending.has(url2) && this.pending.size >= this.maxTrackedUrls) {
1401
- const oldest = this.pending.keys().next();
1402
- if (!oldest.done) {
1403
- this.pending.delete(oldest.value);
1830
+ untrack(key) {
1831
+ const previous = this.pending.get(key);
1832
+ const existed = previous !== void 0;
1833
+ this.pending.delete(key);
1834
+ try {
1835
+ this.persist();
1836
+ } catch (error) {
1837
+ if (existed) {
1838
+ this.pending.set(key, previous);
1404
1839
  }
1840
+ throw error;
1405
1841
  }
1406
- this.pending.set(url2, entry);
1407
- this.persist();
1408
1842
  }
1409
- untrack(url2) {
1410
- this.pending.delete(url2);
1411
- this.persist();
1843
+ clearDelivered(key) {
1844
+ const previous = this.pending.get(key);
1845
+ this.pending.delete(key);
1846
+ try {
1847
+ this.persist();
1848
+ } catch (error) {
1849
+ if (previous !== void 0) {
1850
+ this.pending.set(key, previous);
1851
+ }
1852
+ console.error(
1853
+ `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
1854
+ );
1855
+ }
1412
1856
  }
1413
1857
  persist() {
1414
1858
  if (this.stateStore === null) {
1415
1859
  return;
1416
1860
  }
1417
- this.stateStore.save(
1418
- [...this.pending.entries()].map(([url2, entry]) => ({ url: url2, ...entry }))
1419
- );
1420
- }
1421
- truncated(text) {
1422
- return text.length > this.maxBodyChars ? `${text.slice(0, this.maxBodyChars)}
1423
- ... (truncated)` : text;
1861
+ this.stateStore.save([...this.pending.values()]);
1424
1862
  }
1425
1863
  };
1426
- function receiptTransaction(headers) {
1427
- try {
1428
- const receiptHeader = headers.get(PAYMENT_RESPONSE_HEADER);
1429
- if (receiptHeader === null) {
1430
- return null;
1864
+ function pendingPaymentKey(input) {
1865
+ return `${input.method}:${input.url}:${input.requestBodyHash}`;
1866
+ }
1867
+
1868
+ // ../../src/client/relayer-payer.ts
1869
+ function createRelayerX402Payer(config) {
1870
+ const realizer = new RelayerYieldRealizer({
1871
+ facilitatorBaseUrl: config.facilitatorBaseUrl,
1872
+ signer: config.signer,
1873
+ rpc: config.rpc
1874
+ });
1875
+ return new StandardX402Payer({
1876
+ realizer,
1877
+ x402Fetch: config.x402Fetch,
1878
+ defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc,
1879
+ ...config.stateStore === void 0 ? {} : { stateStore: config.stateStore }
1880
+ });
1881
+ }
1882
+
1883
+ // ../../src/client/standard-x402-state-store.ts
1884
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1885
+ import { basename, dirname, join } from "node:path";
1886
+ function fileStandardX402StateStore(path) {
1887
+ return {
1888
+ load() {
1889
+ let text;
1890
+ try {
1891
+ text = readFileSync(path, "utf8");
1892
+ } catch (error) {
1893
+ if (isMissingFileError(error)) {
1894
+ return [];
1895
+ }
1896
+ throw error;
1897
+ }
1898
+ const parsed = JSON.parse(text);
1899
+ if (!Array.isArray(parsed)) {
1900
+ throw new Error(`pending payment state is not an array: ${path}`);
1901
+ }
1902
+ for (const [index, record] of parsed.entries()) {
1903
+ if (!isPendingPaymentRecord(record)) {
1904
+ throw new Error(
1905
+ `pending payment state has an invalid record at index ${index}: ${path}`
1906
+ );
1907
+ }
1908
+ }
1909
+ return parsed;
1910
+ },
1911
+ save(records) {
1912
+ const directory = dirname(path);
1913
+ mkdirSync(directory, { recursive: true });
1914
+ const tempPath = join(
1915
+ directory,
1916
+ `.${basename(path)}.${process.pid}.${Date.now()}.tmp`
1917
+ );
1918
+ writeFileSync(tempPath, JSON.stringify(records, null, 2));
1919
+ renameSync(tempPath, path);
1431
1920
  }
1432
- const receipt = decodeX402Header(receiptHeader);
1433
- return typeof receipt.transaction === "string" && receipt.transaction.length > 0 ? receipt.transaction : null;
1434
- } catch {
1435
- return null;
1921
+ };
1922
+ }
1923
+ function isMissingFileError(error) {
1924
+ return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
1925
+ }
1926
+ function isPendingPaymentRecord(value) {
1927
+ if (value === null || typeof value !== "object") {
1928
+ return false;
1436
1929
  }
1930
+ const record = value;
1931
+ return typeof record.key === "string" && typeof record.url === "string" && typeof record.method === "string" && typeof record.requestBodyHash === "string" && typeof record.amountRawUsdc === "string" && typeof record.payTo === "string" && (record.feePayer === null || typeof record.feePayer === "string") && typeof record.realizedRawUsdc === "string" && (record.realizeTxSignature === null || typeof record.realizeTxSignature === "string") && (record.status === "realized" || record.status === "external_outcome_unknown") && typeof record.createdAtMs === "number" && typeof record.updatedAtMs === "number";
1437
1932
  }
1438
1933
 
1439
- // src/solana/keys.ts
1440
- import { readFileSync } from "node:fs";
1934
+ // ../../src/solana/keys.ts
1935
+ import { readFileSync as readFileSync2 } from "node:fs";
1441
1936
  import bs586 from "bs58";
1442
1937
  import {
1443
1938
  createKeyPairSignerFromBytes
@@ -1452,7 +1947,7 @@ async function loadKeyPairSigner(params) {
1452
1947
  return createKeyPairSignerFromBytes(bytes);
1453
1948
  }
1454
1949
  if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1455
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1950
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
1456
1951
  if (!Array.isArray(raw) || raw.length !== 64) {
1457
1952
  throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1458
1953
  }
@@ -1460,304 +1955,127 @@ async function loadKeyPairSigner(params) {
1460
1955
  }
1461
1956
  throw new Error(`${label} keypair is not configured`);
1462
1957
  }
1958
+ function loadSecretKeyBytes(params) {
1959
+ const { base58Secret, jsonFilePath, label } = params;
1960
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
1961
+ const bytes = bs586.decode(base58Secret);
1962
+ if (bytes.length !== 64) {
1963
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
1964
+ }
1965
+ return bytes;
1966
+ }
1967
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1968
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
1969
+ if (!Array.isArray(raw) || raw.length !== 64) {
1970
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1971
+ }
1972
+ return Uint8Array.from(raw);
1973
+ }
1974
+ throw new Error(`${label} keypair is not configured`);
1975
+ }
1463
1976
 
1464
- // src/solana/rpc.ts
1977
+ // ../../src/solana/rpc.ts
1465
1978
  import { createSolanaRpc } from "@solana/kit";
1466
1979
  function createRpc(url2) {
1467
1980
  return createSolanaRpc(url2);
1468
1981
  }
1469
1982
 
1470
- // src/x402/client.ts
1471
- var X402ClientError = class extends Error {
1472
- reason;
1473
- detail;
1474
- constructor(reason, message, detail) {
1475
- super(message);
1476
- this.name = "X402ClientError";
1477
- this.reason = reason;
1478
- this.detail = detail;
1479
- }
1480
- };
1481
- var SublyX402Client = class {
1482
- config;
1483
- signer;
1484
- lookupTablesFor;
1485
- fetchImpl;
1486
- constructor(config) {
1487
- this.config = {
1488
- facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
1489
- network: config.network ?? SOLANA_MAINNET_NETWORK
1490
- };
1491
- this.signer = config.signer;
1492
- this.lookupTablesFor = config.lookupTablesFor ?? null;
1493
- this.fetchImpl = config.fetchImpl ?? fetch;
1494
- }
1495
- /**
1496
- * Builds the PAYMENT-SIGNATURE header for a 402 challenge. The request
1497
- * method, URL, and body must be exactly the request being retried; they are
1498
- * bound into the payment and verified again by the seller and facilitator.
1499
- */
1500
- async buildPaymentSignatureHeader(input) {
1501
- const requirement = this.selectRequirement(input.paymentRequiredHeader);
1502
- if (requirement.resource !== input.url) {
1503
- throw new X402ClientError(
1504
- "resource_mismatch",
1505
- "PAYMENT-REQUIRED resource does not match the request URL"
1506
- );
1507
- }
1508
- const requestBodyHash = requestBodyHashFor(input.body);
1509
- const prepared = await this.preparePayment({
1510
- requirement,
1511
- httpMethod: input.httpMethod.toUpperCase(),
1512
- requestBodyHash
1513
- });
1514
- const lookupTables = this.lookupTablesFor === null ? void 0 : await this.lookupTablesFor(prepared.intentJson.serializedTransaction);
1515
- const signed = await this.signer.signPayment({
1516
- intent: prepared.intentJson.signingIntent,
1517
- serializedTransaction: prepared.intentJson.serializedTransaction,
1518
- lookupTables
1519
- });
1520
- const payload = {
1521
- x402Version: X402_VERSION,
1522
- scheme: requirement.scheme,
1523
- network: requirement.network,
1524
- payload: {
1525
- paymentId: prepared.paymentId,
1526
- requestBindingHash: prepared.requestBindingHash,
1527
- preparedMessageHash: prepared.preparedMessageHash,
1528
- serializedTransaction: signed.serializedTransaction,
1529
- agentSignature: signed.agentSignature,
1530
- temporarySettlementSignature: prepared.temporarySettlementSignature
1531
- }
1532
- };
1533
- return {
1534
- headerValue: encodeX402Header(payload),
1535
- paymentId: prepared.paymentId
1536
- };
1537
- }
1538
- /**
1539
- * Convenience wrapper: performs the request, and on a 402 with a Subly
1540
- * requirement pays from vault yield and retries once.
1541
- */
1542
- async fetchWithPayment(url2, init) {
1543
- const first = await this.fetchImpl(url2, init);
1544
- if (first.status !== 402) {
1545
- return first;
1546
- }
1547
- const challenge = first.headers.get(PAYMENT_REQUIRED_HEADER);
1548
- if (challenge === null) {
1549
- return first;
1550
- }
1551
- const { headerValue } = await this.buildPaymentSignatureHeader({
1552
- paymentRequiredHeader: challenge,
1553
- httpMethod: init?.method ?? "GET",
1554
- url: url2,
1555
- body: init?.body ?? null
1556
- });
1557
- return this.fetchImpl(url2, {
1558
- ...init,
1559
- headers: {
1560
- ...init?.headers,
1561
- [PAYMENT_SIGNATURE_HEADER]: headerValue
1562
- }
1563
- });
1564
- }
1565
- selectRequirement(paymentRequiredHeader) {
1566
- let sublyRequirements;
1567
- try {
1568
- sublyRequirements = decodePaymentRequiredHeader(
1569
- paymentRequiredHeader
1570
- ).sublyRequirements;
1571
- } catch (error) {
1572
- throw new X402ClientError(
1573
- error instanceof X402HeaderError ? error.reason : "invalid_challenge",
1574
- "Cannot decode the PAYMENT-REQUIRED header"
1575
- );
1576
- }
1577
- const selected = sublyRequirements.find(
1578
- (candidate) => candidate.network === this.config.network && candidate.extra.vault === SUBLY_VAULT.address && candidate.extra.shareMint === SUBLY_VAULT.shareMint && candidate.asset === SUBLY_VAULT.usdcMint
1579
- ) ?? null;
1580
- if (selected === null) {
1581
- throw new X402ClientError(
1582
- "no_supported_requirement",
1583
- "The 402 challenge contains no supported subly-yield-exact requirement"
1584
- );
1585
- }
1586
- return selected;
1587
- }
1588
- async preparePayment(input) {
1589
- const { requirement } = input;
1590
- const wallet = this.signer.walletAddress;
1591
- const prepareUrl = `${this.config.facilitatorBaseUrl}/v1/payments/prepare`;
1592
- const prepareBody = JSON.stringify({
1593
- wallet,
1594
- scheme: requirement.scheme,
1595
- network: requirement.network,
1596
- vault: requirement.extra.vault,
1597
- shareMint: requirement.extra.shareMint,
1598
- asset: requirement.asset,
1599
- seller: requirement.extra.seller,
1600
- sellerRequestId: requirement.extra.sellerRequestId,
1601
- httpMethod: input.httpMethod,
1602
- canonicalResourceUrl: requirement.resource,
1603
- requestBodyHash: input.requestBodyHash,
1604
- amountRawUsdc: requirement.amountRawUsdc,
1605
- payTo: requirement.payTo,
1606
- sellerUsdcAta: requirement.extra.sellerUsdcAta,
1607
- dustRecipientUsdcAta: deriveAssociatedTokenAddress({
1608
- owner: wallet,
1609
- mint: SUBLY_VAULT.usdcMint
1610
- })
1611
- });
1612
- const response = await this.fetchImpl(prepareUrl, {
1613
- method: "POST",
1614
- headers: {
1615
- ...await walletAuthHeaders({
1616
- signer: this.signer,
1617
- method: "POST",
1618
- url: prepareUrl,
1619
- body: prepareBody
1620
- }),
1621
- "content-type": "application/json"
1622
- },
1623
- body: prepareBody
1983
+ // src/svm-x402-fetch.ts
1984
+ import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
1985
+ import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
1986
+ import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
1987
+ async function createSvmX402Fetch(params) {
1988
+ const signer2 = toClientSvmSigner(
1989
+ await createKeyPairSignerFromBytes2(params.agentSecretKey)
1990
+ );
1991
+ return (url2, init, expected) => {
1992
+ const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
1993
+ schemes: [
1994
+ {
1995
+ network: expected.requirement.network,
1996
+ client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
1997
+ }
1998
+ ],
1999
+ paymentRequirementsSelector: (_x402Version, requirements) => selectExpectedRequirement(requirements, expected)
1624
2000
  });
1625
- const body = await response.json();
1626
- if (response.status !== 200) {
1627
- const error = body.error;
1628
- throw new X402ClientError(
1629
- typeof error?.code === "string" ? error.code : "prepare_failed",
1630
- "Payment preparation failed at the facilitator",
1631
- body
1632
- );
1633
- }
1634
- const prepared = body;
1635
- if (typeof prepared.paymentId !== "string" || typeof prepared.preparedMessageHash !== "string" || typeof prepared.intentJson?.serializedTransaction !== "string" || typeof prepared.intentJson?.signingIntent !== "object") {
1636
- throw new X402ClientError(
1637
- "invalid_prepare_response",
1638
- "Facilitator prepare response is missing required fields"
1639
- );
1640
- }
1641
- return prepared;
2001
+ return wrapped(url2, init);
2002
+ };
2003
+ }
2004
+ function selectExpectedRequirement(requirements, expected) {
2005
+ const match = requirements.find(
2006
+ (candidate) => standardRequirementMatchesSelected(candidate, expected)
2007
+ );
2008
+ if (match === void 0) {
2009
+ throw new Error(
2010
+ "x402 challenge changed after preflight; refusing to pay an unchecked requirement"
2011
+ );
1642
2012
  }
1643
- };
2013
+ return match;
2014
+ }
1644
2015
 
1645
- // demo/shared.ts
2016
+ // src/pay.ts
1646
2017
  function fail(message) {
1647
- console.error(message);
2018
+ process.stderr.write(`${message}
2019
+ `);
1648
2020
  process.exit(1);
1649
2021
  }
1650
-
1651
- // demo/pay.ts
1652
2022
  var url = process.argv[2];
1653
2023
  if (url === void 0 || !/^https?:\/\//.test(url)) {
1654
- fail("Usage: npm run demo:pay -- <url> [maxAmountRawUsdc]");
2024
+ fail("Usage: pay fetch <url> [maxAmountRawUsdc]");
1655
2025
  }
1656
2026
  var maxAmountArg = process.argv[3];
1657
- var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
2027
+ var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1658
2028
  var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
1659
2029
  var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
1660
- var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? "demo/env/mcp-pending-payments.json";
2030
+ var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? join2(homedir(), ".subly", "standard-x402-pending.json");
1661
2031
  var keyPairSigner = await loadKeyPairSigner({
1662
2032
  base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1663
2033
  jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1664
2034
  label: "SUBLY_DEMO_AGENT_KEYPAIR"
1665
2035
  });
2036
+ var agentSecretKey = loadSecretKeyBytes({
2037
+ base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
2038
+ jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
2039
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
2040
+ });
1666
2041
  var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1667
2042
  var rpc = createRpc(rpcUrl);
1668
- async function fetchBudget() {
1669
- try {
1670
- const budgetUrl = `${facilitatorBaseUrl}/v1/wallets/${signer.walletAddress}/budget`;
1671
- const response = await fetch(budgetUrl, {
1672
- headers: await walletAuthHeaders({ signer, method: "GET", url: budgetUrl })
1673
- });
1674
- if (response.status !== 200) {
1675
- return null;
1676
- }
1677
- const body = await response.json();
1678
- return body.budget === void 0 ? null : {
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 statusUrl = `${facilitatorBaseUrl}/v1/payments/${paymentId}`;
1691
- const response = await fetch(statusUrl, {
1692
- headers: await walletAuthHeaders({ signer, method: "GET", url: statusUrl })
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-pay] failed to persist pending payments: ${error instanceof Error ? error.message : String(error)}`
1725
- );
1726
- }
1727
- }
1728
- };
1729
- }
1730
- var service = new PaidFetchService({
1731
- signatureBuilder: new SublyX402Client({
1732
- facilitatorBaseUrl,
1733
- signer,
1734
- lookupTablesFor: (serializedTransaction) => fetchLookupTablesForTransaction(rpc, serializedTransaction)
1735
- }),
2043
+ var payer = createRelayerX402Payer({
2044
+ facilitatorBaseUrl: relayerBaseUrl,
2045
+ signer,
2046
+ rpc,
2047
+ x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
1736
2048
  defaultMaxAmountRawUsdc,
1737
- fetchBudget,
1738
- paymentStatusFor,
1739
- stateStore: fileStateStore(pendingStatePath)
2049
+ stateStore: fileStandardX402StateStore(pendingStatePath)
1740
2050
  });
1741
- console.error(`[subly-pay] agent ${signer.walletAddress} -> ${facilitatorBaseUrl}`);
2051
+ var method = process.env.SUBLY_PAY_METHOD;
2052
+ var body = process.env.SUBLY_PAY_BODY;
2053
+ console.error(`[pay] agent ${signer.walletAddress} -> ${url}`);
1742
2054
  try {
1743
- await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
2055
+ await ensureWalletOnboarded({ facilitatorBaseUrl: relayerBaseUrl, signer });
1744
2056
  } catch (error) {
1745
2057
  console.error(
1746
- `[subly-pay] onboarding skipped: ${error instanceof Error ? error.message : String(error)}`
2058
+ `[pay] onboarding skipped: ${error instanceof Error ? error.message : String(error)}`
1747
2059
  );
1748
2060
  }
1749
2061
  try {
1750
- const result = await service.paidFetch({
2062
+ const result = await payer.pay({
1751
2063
  url,
1752
- ...maxAmountArg === void 0 ? {} : { maxAmountRawUsdc: BigInt(maxAmountArg) }
2064
+ ...method === void 0 ? {} : { method },
2065
+ ...body === void 0 ? {} : { body, headers: { "content-type": "application/json" } },
2066
+ ...maxAmountArg === void 0 ? {} : { maxAmountRawUsdc: BigInt(maxAmountArg) },
2067
+ ...process.env.SUBLY_PAY_FORCE_NEW_PAYMENT === "1" ? { forceNewPayment: true } : {}
1753
2068
  });
1754
2069
  process.stdout.write(`${JSON.stringify(result, null, 2)}
1755
2070
  `);
2071
+ if (!result.paid) {
2072
+ process.exit(1);
2073
+ }
1756
2074
  } catch (error) {
1757
- if (error instanceof PaidFetchError) {
2075
+ if (error instanceof StandardX402PayError) {
1758
2076
  process.stdout.write(
1759
2077
  `${JSON.stringify(
1760
- { paid: false, refused: true, reason: error.reason, message: error.message, detail: error.detail },
2078
+ { paid: false, reason: error.reason, message: error.message },
1761
2079
  null,
1762
2080
  2
1763
2081
  )}
@@ -1765,5 +2083,5 @@ try {
1765
2083
  );
1766
2084
  process.exit(1);
1767
2085
  }
1768
- fail(`[subly-pay] ${error instanceof Error ? error.message : String(error)}`);
2086
+ fail(`[pay] ${error instanceof Error ? error.message : String(error)}`);
1769
2087
  }