@subly_fi/pay 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,7 @@
1
+ // src/mcp-server.ts
2
+ import { homedir } from "node:os";
3
+ import { join as join2 } from "node:path";
4
+
1
5
  // ../../src/client/agent-wallet-signer.ts
2
6
  import { signBytes } from "@solana/kit";
3
7
  import bs584 from "bs58";
@@ -19,6 +23,7 @@ import {
19
23
 
20
24
  // ../../src/lib/hash.ts
21
25
  import { createHash } from "node:crypto";
26
+ var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
22
27
  function sha256TaggedHex(data) {
23
28
  return `sha256-${createHash("sha256").update(data).digest("hex")}`;
24
29
  }
@@ -115,12 +120,12 @@ function deriveAssociatedTokenAddress(params) {
115
120
  "associatedTokenProgramId"
116
121
  );
117
122
  for (let bump = 255; bump >= 0; bump -= 1) {
118
- const address3 = createProgramAddress(
123
+ const address2 = createProgramAddress(
119
124
  [owner, tokenProgramId, mint, Uint8Array.of(bump)],
120
125
  associatedTokenProgramId
121
126
  );
122
- if (address3 !== null) {
123
- return bs582.encode(address3);
127
+ if (address2 !== null) {
128
+ return bs582.encode(address2);
124
129
  }
125
130
  }
126
131
  throw new Error("Unable to derive associated token account address");
@@ -1001,7 +1006,7 @@ var OnboardingError = class extends Error {
1001
1006
  };
1002
1007
  async function ensureWalletOnboarded(params) {
1003
1008
  const fetchImpl = params.fetchImpl ?? fetch;
1004
- const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
1009
+ const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
1005
1010
  const post = async (step, path, body) => {
1006
1011
  const url = `${baseUrl}${path}`;
1007
1012
  const serialized = JSON.stringify(body);
@@ -1110,6 +1115,14 @@ function decodeX402Header(headerValue) {
1110
1115
  );
1111
1116
  }
1112
1117
  }
1118
+ function requestBodyHashFor(body) {
1119
+ if (body === null || body === void 0 || body.length === 0) {
1120
+ return EMPTY_BODY_HASH;
1121
+ }
1122
+ return sha256TaggedHex(
1123
+ typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1124
+ );
1125
+ }
1113
1126
 
1114
1127
  // ../../src/client/paid-fetch.ts
1115
1128
  function formatRawUsdcAmount(raw) {
@@ -1202,6 +1215,24 @@ function selectPayableSolanaRequirement(requirements, options) {
1202
1215
  feePayer: requirement.extra?.feePayer ?? null
1203
1216
  };
1204
1217
  }
1218
+ function standardRequirementMatchesSelected(candidate, selected) {
1219
+ const parsed = standardExactRequirementSchema.safeParse(candidate);
1220
+ return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
1221
+ }
1222
+ function stableJson(value) {
1223
+ return JSON.stringify(sortJson(value));
1224
+ }
1225
+ function sortJson(value) {
1226
+ if (Array.isArray(value)) {
1227
+ return value.map(sortJson);
1228
+ }
1229
+ if (value !== null && typeof value === "object") {
1230
+ return Object.fromEntries(
1231
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
1232
+ );
1233
+ }
1234
+ return value;
1235
+ }
1205
1236
 
1206
1237
  // ../../src/client/standard-x402-payer.ts
1207
1238
  var StandardX402PayError = class extends Error {
@@ -1221,6 +1252,10 @@ var StandardX402Payer = class {
1221
1252
  defaultMaxAmountRawUsdc;
1222
1253
  network;
1223
1254
  usdcMint;
1255
+ stateStore;
1256
+ pending = /* @__PURE__ */ new Map();
1257
+ inFlight = /* @__PURE__ */ new Map();
1258
+ nowMs;
1224
1259
  constructor(config) {
1225
1260
  this.realizer = config.realizer;
1226
1261
  this.x402Fetch = config.x402Fetch;
@@ -1228,10 +1263,59 @@ var StandardX402Payer = class {
1228
1263
  this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1229
1264
  this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1230
1265
  this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1266
+ this.stateStore = config.stateStore ?? null;
1267
+ this.nowMs = config.nowMs ?? (() => Date.now());
1268
+ if (this.stateStore !== null) {
1269
+ for (const record of this.stateStore.load()) {
1270
+ this.pending.set(record.key, record);
1271
+ }
1272
+ }
1231
1273
  }
1232
- async pay(input) {
1274
+ pay(input) {
1275
+ const method = (input.method ?? "GET").toUpperCase();
1276
+ const requestBodyHash = requestBodyHashFor(input.body ?? null);
1277
+ const pendingKey = pendingPaymentKey({
1278
+ url: input.url,
1279
+ method,
1280
+ requestBodyHash
1281
+ });
1282
+ const existingFlow = this.inFlight.get(pendingKey);
1283
+ if (existingFlow !== void 0) {
1284
+ return existingFlow;
1285
+ }
1286
+ const flow = this.run(input, {
1287
+ method,
1288
+ requestBodyHash,
1289
+ pendingKey
1290
+ }).finally(() => {
1291
+ this.inFlight.delete(pendingKey);
1292
+ });
1293
+ this.inFlight.set(pendingKey, flow);
1294
+ return flow;
1295
+ }
1296
+ async run(input, computed) {
1297
+ const { method, requestBodyHash, pendingKey } = computed;
1298
+ const existingPending = this.pending.get(pendingKey);
1299
+ if (existingPending !== void 0 && input.forceNewPayment !== true) {
1300
+ throw new StandardX402PayError(
1301
+ "payment_outcome_unknown",
1302
+ "a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
1303
+ existingPending
1304
+ );
1305
+ }
1306
+ if (existingPending !== void 0 && input.forceNewPayment === true) {
1307
+ try {
1308
+ this.untrack(pendingKey);
1309
+ } catch (error) {
1310
+ throw new StandardX402PayError(
1311
+ "state_persist_failed",
1312
+ "could not clear the previous pending x402 marker before forcing a new payment",
1313
+ error
1314
+ );
1315
+ }
1316
+ }
1233
1317
  const init = {
1234
- method: input.method ?? "GET",
1318
+ method,
1235
1319
  ...input.body === void 0 ? {} : { body: input.body },
1236
1320
  ...input.headers === void 0 ? {} : { headers: input.headers }
1237
1321
  };
@@ -1260,15 +1344,55 @@ var StandardX402Payer = class {
1260
1344
  error
1261
1345
  );
1262
1346
  }
1263
- const response = await this.x402Fetch(input.url, init);
1347
+ const pendingRecord = {
1348
+ key: pendingKey,
1349
+ url: input.url,
1350
+ method,
1351
+ requestBodyHash,
1352
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1353
+ payTo: selected.payTo,
1354
+ feePayer: selected.feePayer,
1355
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1356
+ realizeTxSignature: realized.txSignature,
1357
+ status: "realized",
1358
+ createdAtMs: this.nowMs(),
1359
+ updatedAtMs: this.nowMs()
1360
+ };
1361
+ try {
1362
+ this.track(pendingRecord);
1363
+ } catch (error) {
1364
+ throw new StandardX402PayError(
1365
+ "state_persist_failed",
1366
+ "could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
1367
+ { error, pendingPayment: pendingRecord }
1368
+ );
1369
+ }
1370
+ let response;
1371
+ try {
1372
+ response = await this.x402Fetch(input.url, init, selected);
1373
+ } catch (error) {
1374
+ const persistError = this.tryMarkUnknown(pendingKey, {
1375
+ message: error instanceof Error ? error.message : String(error)
1376
+ });
1377
+ throw new StandardX402PayError(
1378
+ "payment_outcome_unknown",
1379
+ `the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
1380
+ { error, persistError }
1381
+ );
1382
+ }
1264
1383
  const bodyText = await response.text();
1265
1384
  if (response.status !== 200) {
1385
+ const persistError = this.tryMarkUnknown(pendingKey, {
1386
+ status: response.status,
1387
+ body: bodyText
1388
+ });
1266
1389
  throw new StandardX402PayError(
1267
- "delivery_failed",
1268
- `the x402 payment did not deliver (status ${response.status})`,
1269
- { status: response.status, body: bodyText }
1390
+ "payment_outcome_unknown",
1391
+ `the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
1392
+ { status: response.status, body: bodyText, persistError }
1270
1393
  );
1271
1394
  }
1395
+ this.clearDelivered(pendingKey);
1272
1396
  return {
1273
1397
  paid: true,
1274
1398
  status: response.status,
@@ -1314,26 +1438,421 @@ var StandardX402Payer = class {
1314
1438
  );
1315
1439
  }
1316
1440
  }
1441
+ track(record) {
1442
+ const previous = this.pending.get(record.key);
1443
+ this.pending.set(record.key, record);
1444
+ try {
1445
+ this.persist();
1446
+ } catch (error) {
1447
+ if (previous === void 0) {
1448
+ this.pending.delete(record.key);
1449
+ } else {
1450
+ this.pending.set(record.key, previous);
1451
+ }
1452
+ throw error;
1453
+ }
1454
+ }
1455
+ markUnknown(key, detail) {
1456
+ const current = this.pending.get(key);
1457
+ if (current === void 0) {
1458
+ return;
1459
+ }
1460
+ const next = {
1461
+ ...current,
1462
+ status: "external_outcome_unknown",
1463
+ updatedAtMs: this.nowMs(),
1464
+ detail
1465
+ };
1466
+ this.pending.set(key, next);
1467
+ try {
1468
+ this.persist();
1469
+ } catch (error) {
1470
+ this.pending.set(key, current);
1471
+ throw error;
1472
+ }
1473
+ }
1474
+ tryMarkUnknown(key, detail) {
1475
+ try {
1476
+ this.markUnknown(key, detail);
1477
+ return null;
1478
+ } catch (error) {
1479
+ return error;
1480
+ }
1481
+ }
1482
+ untrack(key) {
1483
+ const previous = this.pending.get(key);
1484
+ const existed = previous !== void 0;
1485
+ this.pending.delete(key);
1486
+ try {
1487
+ this.persist();
1488
+ } catch (error) {
1489
+ if (existed) {
1490
+ this.pending.set(key, previous);
1491
+ }
1492
+ throw error;
1493
+ }
1494
+ }
1495
+ clearDelivered(key) {
1496
+ const previous = this.pending.get(key);
1497
+ this.pending.delete(key);
1498
+ try {
1499
+ this.persist();
1500
+ } catch (error) {
1501
+ if (previous !== void 0) {
1502
+ this.pending.set(key, previous);
1503
+ }
1504
+ console.error(
1505
+ `[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
1506
+ );
1507
+ }
1508
+ }
1509
+ persist() {
1510
+ if (this.stateStore === null) {
1511
+ return;
1512
+ }
1513
+ this.stateStore.save([...this.pending.values()]);
1514
+ }
1515
+ };
1516
+ function pendingPaymentKey(input) {
1517
+ return `${input.method}:${input.url}:${input.requestBodyHash}`;
1518
+ }
1519
+
1520
+ // ../../src/client/lookup-tables.ts
1521
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1522
+ import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
1523
+ function lookupTableAddressesForTransaction(serializedTransaction) {
1524
+ const wire = Buffer.from(serializedTransaction, "base64");
1525
+ let offset = 0;
1526
+ let signatureCount = 0;
1527
+ let shift = 0;
1528
+ while (offset < wire.length) {
1529
+ const byte = wire[offset];
1530
+ signatureCount |= (byte & 127) << shift;
1531
+ offset += 1;
1532
+ if ((byte & 128) === 0) {
1533
+ break;
1534
+ }
1535
+ shift += 7;
1536
+ }
1537
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
1538
+ const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
1539
+ const lookups = compiled.addressTableLookups ?? [];
1540
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
1541
+ }
1542
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1543
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1544
+ if (addresses.length === 0) {
1545
+ return {};
1546
+ }
1547
+ const tables = await fetchAllMaybeAddressLookupTable(
1548
+ rpc2,
1549
+ addresses.map((value) => address(value))
1550
+ );
1551
+ const result = {};
1552
+ for (const table of tables) {
1553
+ if (table.exists) {
1554
+ result[table.address] = table.data.addresses.map(String);
1555
+ }
1556
+ }
1557
+ return result;
1558
+ }
1559
+
1560
+ // ../../src/client/vault-flows.ts
1561
+ var VaultFlowClientError = class extends Error {
1562
+ constructor(step, message, detail = null) {
1563
+ super(message);
1564
+ this.step = step;
1565
+ this.detail = detail;
1566
+ this.name = "VaultFlowClientError";
1567
+ }
1568
+ step;
1569
+ detail;
1570
+ };
1571
+ var VaultFlowClient = class {
1572
+ baseUrl;
1573
+ signer;
1574
+ fetchImpl;
1575
+ lookupTablesFor;
1576
+ pollTimeoutMs;
1577
+ pollIntervalMs;
1578
+ constructor(config) {
1579
+ this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1580
+ this.signer = config.signer;
1581
+ this.fetchImpl = config.fetchImpl ?? fetch;
1582
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1583
+ this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1584
+ this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1585
+ }
1586
+ /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1587
+ async deposit(input) {
1588
+ const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1589
+ wallet: this.signer.walletAddress,
1590
+ amountRawUsdc: input.amountRawUsdc.toString()
1591
+ });
1592
+ const signed = await this.signer.signDeposit({
1593
+ intent: prepared.signingIntent,
1594
+ serializedTransaction: prepared.serializedTransaction,
1595
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1596
+ });
1597
+ let outcome = await this.postJson("submit", "/v1/deposits/submit", {
1598
+ depositId: prepared.depositId,
1599
+ serializedTransaction: signed.serializedTransaction,
1600
+ agentSignature: signed.agentSignature
1601
+ });
1602
+ if (outcome.status === "submitted") {
1603
+ outcome = await this.pollUntilTerminal(
1604
+ `/v1/deposits/${prepared.depositId}`,
1605
+ outcome
1606
+ );
1607
+ }
1608
+ return {
1609
+ depositId: prepared.depositId,
1610
+ status: outcome.status,
1611
+ txSignature: outcome.txSignature ?? null,
1612
+ actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
1613
+ sharesMintedRaw: outcome.sharesMintedRaw ?? null,
1614
+ errorCode: outcome.errorCode ?? null
1615
+ };
1616
+ }
1617
+ /**
1618
+ * Moves USDC from the vault back to the agent wallet's USDC ATA (fee
1619
+ * sponsored). A plain withdrawal is the exit path and MAY spend principal;
1620
+ * with purpose "yield_realize" the relayer refuses anything beyond the
1621
+ * spendable yield (the payment path, via RelayerYieldRealizer).
1622
+ */
1623
+ async withdraw(input) {
1624
+ const prepared = await this.postJson(
1625
+ "prepare",
1626
+ "/v1/withdrawals/prepare",
1627
+ {
1628
+ wallet: this.signer.walletAddress,
1629
+ amountRawUsdc: input.amountRawUsdc.toString(),
1630
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1631
+ }
1632
+ );
1633
+ const signed = await this.signer.signWithdrawal({
1634
+ intent: prepared.signingIntent,
1635
+ serializedTransaction: prepared.serializedTransaction,
1636
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1637
+ });
1638
+ let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
1639
+ withdrawalId: prepared.withdrawalId,
1640
+ serializedTransaction: signed.serializedTransaction,
1641
+ agentSignature: signed.agentSignature
1642
+ });
1643
+ if (outcome.status === "submitted") {
1644
+ outcome = await this.pollUntilTerminal(
1645
+ `/v1/withdrawals/${prepared.withdrawalId}`,
1646
+ outcome
1647
+ );
1648
+ }
1649
+ return {
1650
+ withdrawalId: prepared.withdrawalId,
1651
+ status: outcome.status,
1652
+ txSignature: outcome.txSignature ?? null,
1653
+ destinationUsdcAta: prepared.destinationUsdcAta,
1654
+ actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
1655
+ actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
1656
+ errorCode: outcome.errorCode ?? null
1657
+ };
1658
+ }
1659
+ /**
1660
+ * Reads the yield budget. Syncs the relayer's ledger from chain first (so
1661
+ * yield accrued since the last sync shows up); the sync is best-effort and
1662
+ * on failure the last-synced view is returned.
1663
+ */
1664
+ async getBudget(options = {}) {
1665
+ if (options.refreshFromChain !== false) {
1666
+ try {
1667
+ await this.postJson(
1668
+ "sync",
1669
+ `/v1/wallets/${this.signer.walletAddress}/sync`,
1670
+ { source: "chain" }
1671
+ );
1672
+ } catch {
1673
+ }
1674
+ }
1675
+ const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1676
+ const response = await this.fetchImpl(url, {
1677
+ headers: await walletAuthHeaders({
1678
+ signer: this.signer,
1679
+ method: "GET",
1680
+ url
1681
+ })
1682
+ });
1683
+ const text = await response.text();
1684
+ if (response.status !== 200) {
1685
+ throw new VaultFlowClientError(
1686
+ "budget",
1687
+ `budget endpoint returned ${response.status}: ${text}`
1688
+ );
1689
+ }
1690
+ let parsed;
1691
+ try {
1692
+ parsed = JSON.parse(text);
1693
+ } catch {
1694
+ throw new VaultFlowClientError(
1695
+ "budget",
1696
+ "budget endpoint returned 200 with a non-JSON body",
1697
+ text
1698
+ );
1699
+ }
1700
+ const body = parsed;
1701
+ return {
1702
+ wallet: this.signer.walletAddress,
1703
+ principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
1704
+ positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
1705
+ grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
1706
+ spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1707
+ };
1708
+ }
1709
+ /**
1710
+ * Polls the reconciling GET endpoint until the intent leaves "submitted"
1711
+ * (each read looks the tx up on-chain) or the timeout elapses.
1712
+ */
1713
+ async pollUntilTerminal(path, last) {
1714
+ const deadline = Date.now() + this.pollTimeoutMs;
1715
+ let latest = last;
1716
+ while (Date.now() < deadline) {
1717
+ await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
1718
+ const url = `${this.baseUrl}${path}`;
1719
+ const response = await this.fetchImpl(url, {
1720
+ headers: await walletAuthHeaders({
1721
+ signer: this.signer,
1722
+ method: "GET",
1723
+ url
1724
+ })
1725
+ });
1726
+ if (response.status !== 200) {
1727
+ continue;
1728
+ }
1729
+ try {
1730
+ latest = await response.json();
1731
+ } catch {
1732
+ continue;
1733
+ }
1734
+ if (latest.status !== "submitted") {
1735
+ return latest;
1736
+ }
1737
+ }
1738
+ return latest;
1739
+ }
1740
+ async postJson(step, path, body) {
1741
+ const url = `${this.baseUrl}${path}`;
1742
+ const serialized = JSON.stringify(body);
1743
+ const response = await this.fetchImpl(url, {
1744
+ method: "POST",
1745
+ headers: {
1746
+ ...await walletAuthHeaders({
1747
+ signer: this.signer,
1748
+ method: "POST",
1749
+ url,
1750
+ body: serialized
1751
+ }),
1752
+ "content-type": "application/json"
1753
+ },
1754
+ body: serialized
1755
+ });
1756
+ const text = await response.text();
1757
+ if (response.status !== 200) {
1758
+ throw new VaultFlowClientError(
1759
+ step,
1760
+ `${path} failed with ${response.status}: ${text}`,
1761
+ text
1762
+ );
1763
+ }
1764
+ try {
1765
+ return JSON.parse(text);
1766
+ } catch {
1767
+ throw new VaultFlowClientError(
1768
+ step,
1769
+ `${path} returned 200 with a non-JSON body`,
1770
+ text
1771
+ );
1772
+ }
1773
+ }
1317
1774
  };
1318
1775
 
1319
1776
  // ../../src/client/mcp-payment-server.ts
1320
1777
  var TOOL_NAME = "fetch_with_subly_payment";
1778
+ var DEPOSIT_TOOL_NAME = "deposit_to_subly_vault";
1779
+ var WITHDRAW_TOOL_NAME = "withdraw_from_subly_vault";
1780
+ var BUDGET_TOOL_NAME = "get_subly_yield_budget";
1321
1781
  var SERVER_INSTRUCTIONS = `Subly lets an agent pay for ANY standard x402 (HTTP 402) paid API from its wallet's Kamino vault YIELD \u2014 the deposited principal is never spent, and the seller needs no Subly integration.
1322
1782
 
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.
1783
+ One-time setup: the operator needs a Solana keypair for the agent wallet. Subly does NOT create wallets; make one with \`solana-keygen new -o agent.json\` (or export a keypair from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it. The private key never leaves that file; this server only signs locally with it. Then fund the wallet with USDC on Solana mainnet \u2014 no SOL is ever needed, all vault transaction fees are sponsored.
1327
1784
 
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.`;
1785
+ From there the agent can do everything with these tools:
1786
+ 1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (minimum just over 1 USDC, e.g. 1010000 raw) so it starts earning yield.
1787
+ 2. get_subly_yield_budget() shows the principal, position value, and the spendable yield a payment can use right now.
1788
+ 3. fetch_with_subly_payment(url) GETs or POSTs a paid resource from any x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's standard x402 challenge, returning the body plus the payment details. If it returns insufficient_yield, that is expected \u2014 yield accrues over time; wait, do not loop.
1789
+ 4. withdraw_from_subly_vault(amountRawUsdc) exits: moves vault funds (principal included) back to the agent wallet's USDC account.`;
1329
1790
  async function runMcpPaymentServer(config) {
1330
- const { payer: payer2, signer: signer2, facilitatorBaseUrl: facilitatorBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1791
+ const { payer: payer2, signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1792
+ const vaultFlows = config.vaultFlows ?? null;
1331
1793
  const server = new Server(
1332
1794
  { name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
1333
1795
  { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1334
1796
  );
1797
+ const vaultTools = vaultFlows === null ? [] : [
1798
+ {
1799
+ name: DEPOSIT_TOOL_NAME,
1800
+ description: "Deposit USDC from the agent wallet into the Subly/Kamino vault so it starts earning the yield that funds x402 payments. The transaction fee is sponsored \u2014 the agent wallet needs USDC only, never SOL. The vault minimum is just over 1 USDC: share rounding refuses exactly 1000000 raw, so deposit e.g. 1010000 (1.01 USDC) or more. The deposited amount becomes protected principal: payments can only ever spend the yield on top of it.",
1801
+ inputSchema: {
1802
+ type: "object",
1803
+ properties: {
1804
+ amountRawUsdc: {
1805
+ type: "string",
1806
+ description: 'Amount to deposit in raw USDC units (6 decimals, e.g. "1010000" = 1.01 USDC). Must exceed the 1 USDC vault minimum by a small rounding margin.'
1807
+ }
1808
+ },
1809
+ required: ["amountRawUsdc"]
1810
+ },
1811
+ annotations: {
1812
+ title: "Deposit into the Subly vault",
1813
+ readOnlyHint: false,
1814
+ destructiveHint: false,
1815
+ idempotentHint: false,
1816
+ openWorldHint: false
1817
+ }
1818
+ },
1819
+ {
1820
+ name: WITHDRAW_TOOL_NAME,
1821
+ description: "Withdraw USDC from the Subly/Kamino vault back to the agent wallet's USDC account (fee sponsored, no SOL needed). This is the exit path and may spend PRINCIPAL \u2014 it reduces the deposit that earns yield. Limited to the vault's instant liquidity.",
1822
+ inputSchema: {
1823
+ type: "object",
1824
+ properties: {
1825
+ amountRawUsdc: {
1826
+ type: "string",
1827
+ description: 'Amount to withdraw in raw USDC units (6 decimals, e.g. "1000000" = 1 USDC).'
1828
+ }
1829
+ },
1830
+ required: ["amountRawUsdc"]
1831
+ },
1832
+ annotations: {
1833
+ title: "Withdraw from the Subly vault",
1834
+ readOnlyHint: false,
1835
+ destructiveHint: true,
1836
+ idempotentHint: false,
1837
+ openWorldHint: false
1838
+ }
1839
+ },
1840
+ {
1841
+ name: BUDGET_TOOL_NAME,
1842
+ description: "Show the agent wallet's Subly vault budget: protected principal, current position value, and the spendable yield available for x402 payments right now. Syncs the position from chain first, so newly accrued yield is included.",
1843
+ inputSchema: { type: "object", properties: {} },
1844
+ annotations: {
1845
+ title: "Get Subly yield budget",
1846
+ readOnlyHint: true,
1847
+ destructiveHint: false,
1848
+ idempotentHint: true,
1849
+ openWorldHint: false
1850
+ }
1851
+ }
1852
+ ];
1335
1853
  server.setRequestHandler(ListToolsRequestSchema, () => ({
1336
1854
  tools: [
1855
+ ...vaultTools,
1337
1856
  {
1338
1857
  name: TOOL_NAME,
1339
1858
  description: `Fetch a URL (GET or POST), automatically paying a standard x402 (HTTP 402) challenge from any x402-compatible seller (Nansen, etc.) out of the agent wallet's Kamino vault yield. Subly realizes just enough yield to the agent's USDC ATA (sponsored) and pays the seller's Solana USDC \`exact\` challenge; the seller needs no Subly integration. Returns the response body and, when a payment was made, the payment details (amount, payee, realize tx). Challenges above maxAmountRawUsdc (default ${defaultMaxAmountRawUsdc2} raw = ${formatRawUsdcAmount(
@@ -1362,6 +1881,10 @@ async function runMcpPaymentServer(config) {
1362
1881
  maxAmountRawUsdc: {
1363
1882
  type: "string",
1364
1883
  description: 'Refuse (without paying) any challenge above this amount in raw USDC units (6 decimals, e.g. "10000" = 0.01 USDC). Defaults to the server-side cap.'
1884
+ },
1885
+ forceNewPayment: {
1886
+ type: "boolean",
1887
+ description: "Pay again even if a previous external x402 attempt for the same URL/method/body has an unknown outcome. This may pay twice for the same resource."
1365
1888
  }
1366
1889
  },
1367
1890
  required: ["url"]
@@ -1376,7 +1899,118 @@ async function runMcpPaymentServer(config) {
1376
1899
  }
1377
1900
  ]
1378
1901
  }));
1902
+ const textResult = (value, isError = false) => ({
1903
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
1904
+ ...isError ? { isError: true } : {}
1905
+ });
1906
+ const vaultFlowFailure = (error) => {
1907
+ if (error instanceof VaultFlowClientError) {
1908
+ return textResult(
1909
+ { ok: false, step: error.step, message: error.message },
1910
+ true
1911
+ );
1912
+ }
1913
+ return {
1914
+ content: [
1915
+ {
1916
+ type: "text",
1917
+ text: error instanceof Error ? error.message : String(error)
1918
+ }
1919
+ ],
1920
+ isError: true
1921
+ };
1922
+ };
1923
+ const parseRawAmount = (value) => {
1924
+ if (typeof value !== "string" && typeof value !== "number") {
1925
+ return null;
1926
+ }
1927
+ try {
1928
+ const amount = BigInt(value);
1929
+ return amount > 0n ? amount : null;
1930
+ } catch {
1931
+ return null;
1932
+ }
1933
+ };
1934
+ const vaultFlowOutcome = (outcome, amountField) => {
1935
+ const solscanUrl = outcome.txSignature === null ? null : `https://solscan.io/tx/${outcome.txSignature}`;
1936
+ if (outcome.status === "submitted") {
1937
+ return textResult(
1938
+ {
1939
+ ...outcome,
1940
+ ...amountField,
1941
+ solscanUrl,
1942
+ stillConfirming: true,
1943
+ warning: "the transaction was broadcast but had not confirmed before the poll timeout. Do NOT submit this deposit/withdrawal again \u2014 it may still confirm and moving the funds twice is not what the user asked for. Check get_subly_yield_budget in a minute, or the solscanUrl."
1944
+ },
1945
+ true
1946
+ );
1947
+ }
1948
+ return textResult(
1949
+ { ...outcome, ...amountField, solscanUrl },
1950
+ outcome.status !== "confirmed"
1951
+ );
1952
+ };
1379
1953
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1954
+ const vaultToolNames = [
1955
+ BUDGET_TOOL_NAME,
1956
+ DEPOSIT_TOOL_NAME,
1957
+ WITHDRAW_TOOL_NAME
1958
+ ];
1959
+ if (vaultFlows !== null && vaultToolNames.includes(request.params.name)) {
1960
+ try {
1961
+ await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
1962
+ } catch {
1963
+ }
1964
+ if (request.params.name === BUDGET_TOOL_NAME) {
1965
+ try {
1966
+ const budget = await vaultFlows.getBudget();
1967
+ return textResult({
1968
+ ...budget,
1969
+ principalUsdc: formatRawUsdcAmount(
1970
+ BigInt(budget.principalBasisRawUsdc)
1971
+ ),
1972
+ positionValueUsdc: formatRawUsdcAmount(
1973
+ BigInt(budget.positionValueRawUsdc)
1974
+ ),
1975
+ spendableYieldUsdc: formatRawUsdcAmount(
1976
+ BigInt(budget.spendableYieldRawUsdc)
1977
+ )
1978
+ });
1979
+ } catch (error) {
1980
+ return vaultFlowFailure(error);
1981
+ }
1982
+ }
1983
+ const amountRawUsdc = parseRawAmount(
1984
+ (request.params.arguments ?? {}).amountRawUsdc
1985
+ );
1986
+ if (amountRawUsdc === null) {
1987
+ return textResult(
1988
+ {
1989
+ ok: false,
1990
+ message: 'amountRawUsdc must be a positive integer raw USDC amount (6 decimals, e.g. "1000000" = 1 USDC)'
1991
+ },
1992
+ true
1993
+ );
1994
+ }
1995
+ try {
1996
+ if (request.params.name === DEPOSIT_TOOL_NAME) {
1997
+ const outcome2 = await vaultFlows.deposit({ amountRawUsdc });
1998
+ return vaultFlowOutcome(outcome2, {
1999
+ depositedUsdc: formatRawUsdcAmount(
2000
+ BigInt(outcome2.actualDepositRawUsdc ?? "0")
2001
+ )
2002
+ });
2003
+ }
2004
+ const outcome = await vaultFlows.withdraw({ amountRawUsdc });
2005
+ return vaultFlowOutcome(outcome, {
2006
+ withdrawnUsdc: formatRawUsdcAmount(
2007
+ BigInt(outcome.actualWithdrawRawUsdc ?? "0")
2008
+ )
2009
+ });
2010
+ } catch (error) {
2011
+ return vaultFlowFailure(error);
2012
+ }
2013
+ }
1380
2014
  if (request.params.name !== TOOL_NAME) {
1381
2015
  return {
1382
2016
  content: [{ type: "text", text: `unknown tool: ${request.params.name}` }],
@@ -1413,6 +2047,7 @@ async function runMcpPaymentServer(config) {
1413
2047
  }
1414
2048
  const method = typeof args.method === "string" ? args.method : void 0;
1415
2049
  const body = typeof args.body === "string" ? args.body : void 0;
2050
+ const forceNewPayment = args.forceNewPayment === true;
1416
2051
  const headers = args.headers !== null && typeof args.headers === "object" && !Array.isArray(args.headers) ? Object.fromEntries(
1417
2052
  Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
1418
2053
  ) : void 0;
@@ -1423,7 +2058,8 @@ async function runMcpPaymentServer(config) {
1423
2058
  ...method === void 0 ? {} : { method },
1424
2059
  ...body === void 0 ? {} : { body },
1425
2060
  ...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
1426
- ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
2061
+ ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
2062
+ ...forceNewPayment ? { forceNewPayment } : {}
1427
2063
  });
1428
2064
  return {
1429
2065
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
@@ -1462,7 +2098,7 @@ async function runMcpPaymentServer(config) {
1462
2098
  }
1463
2099
  });
1464
2100
  try {
1465
- await ensureWalletOnboarded({ facilitatorBaseUrl: facilitatorBaseUrl2, signer: signer2 });
2101
+ await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
1466
2102
  console.error("[subly-mcp] wallet registered and synced at the relayer");
1467
2103
  } catch (error) {
1468
2104
  console.error(
@@ -1472,54 +2108,12 @@ async function runMcpPaymentServer(config) {
1472
2108
  const transport = new StdioServerTransport();
1473
2109
  await server.connect(transport);
1474
2110
  console.error(
1475
- `[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${facilitatorBaseUrl2}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
1476
- );
1477
- }
1478
-
1479
- // ../../src/client/relayer-yield-realizer.ts
1480
- import { address as address2 } from "@solana/kit";
1481
-
1482
- // ../../src/client/lookup-tables.ts
1483
- import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1484
- import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
1485
- function lookupTableAddressesForTransaction(serializedTransaction) {
1486
- const wire = Buffer.from(serializedTransaction, "base64");
1487
- let offset = 0;
1488
- let signatureCount = 0;
1489
- let shift = 0;
1490
- while (offset < wire.length) {
1491
- const byte = wire[offset];
1492
- signatureCount |= (byte & 127) << shift;
1493
- offset += 1;
1494
- if ((byte & 128) === 0) {
1495
- break;
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));
1503
- }
1504
- async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1505
- const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1506
- if (addresses.length === 0) {
1507
- return {};
1508
- }
1509
- const tables = await fetchAllMaybeAddressLookupTable(
1510
- rpc2,
1511
- addresses.map((value) => address(value))
2111
+ `[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${relayerBaseUrl2}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
1512
2112
  );
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;
1520
2113
  }
1521
2114
 
1522
2115
  // ../../src/client/relayer-yield-realizer.ts
2116
+ var REALIZE_OVERHEAD_RAW_USDC = 2500n;
1523
2117
  var RelayerRealizeError = class extends Error {
1524
2118
  constructor(code, message, detail = null) {
1525
2119
  super(message);
@@ -1531,79 +2125,52 @@ var RelayerRealizeError = class extends Error {
1531
2125
  detail;
1532
2126
  };
1533
2127
  var RelayerYieldRealizer = class {
1534
- config;
1535
- signer;
1536
- rpc;
1537
- fetchImpl;
1538
- lookupTablesFor;
2128
+ vaultFlows;
1539
2129
  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));
2130
+ this.vaultFlows = new VaultFlowClient({
2131
+ relayerBaseUrl: config.relayerBaseUrl,
2132
+ signer: config.signer,
2133
+ rpc: config.rpc,
2134
+ ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
2135
+ ...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
2136
+ });
1549
2137
  }
1550
2138
  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
- }
2139
+ const shortfallRawUsdc = input.amountRawUsdc;
1565
2140
  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);
2141
+ let outcome;
2142
+ try {
2143
+ outcome = await this.vaultFlows.withdraw({
2144
+ amountRawUsdc: shortfallRawUsdc,
2145
+ // The relayer refuses to prepare this withdrawal beyond the spendable
2146
+ // yield — the principal-protection guard the client cannot bypass.
2147
+ purpose: "yield_realize"
2148
+ });
2149
+ } catch (error) {
2150
+ throw this.mapWithdrawError(error);
1582
2151
  }
1583
- if (settled.status !== "confirmed" || settled.txSignature === null) {
2152
+ if (outcome.status !== "confirmed" || outcome.txSignature === null) {
1584
2153
  throw new RelayerRealizeError(
1585
2154
  "realize_not_confirmed",
1586
- `yield realize withdrawal did not confirm (status=${settled.status})`,
1587
- settled
2155
+ `yield realize withdrawal did not confirm (status=${outcome.status})`,
2156
+ outcome
1588
2157
  );
1589
2158
  }
1590
2159
  return {
1591
- realizedRawUsdc: BigInt(settled.actualWithdrawRawUsdc ?? "0"),
1592
- txSignature: settled.txSignature
2160
+ realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
2161
+ txSignature: outcome.txSignature
1593
2162
  };
1594
2163
  }
1595
- /** Refuses to realize more than the ledger's spendable yield (principal). */
2164
+ /**
2165
+ * Refuses to realize more than the ledger's spendable yield (principal).
2166
+ * getBudget syncs the relayer's ledger from chain first (best-effort), so a
2167
+ * long-running client sees yield as it accrues instead of a frozen view.
2168
+ */
1596
2169
  async assertSpendableYield(shortfallRawUsdc) {
1597
- const url = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1598
- let response;
2170
+ let spendable;
1599
2171
  try {
1600
- response = await this.fetchImpl(url, {
1601
- headers: await walletAuthHeaders({
1602
- signer: this.signer,
1603
- method: "GET",
1604
- url
1605
- })
1606
- });
2172
+ const budget = await this.vaultFlows.getBudget();
2173
+ spendable = BigInt(budget.spendableYieldRawUsdc);
1607
2174
  } catch (error) {
1608
2175
  throw new RelayerRealizeError(
1609
2176
  "budget_unavailable",
@@ -1611,105 +2178,118 @@ var RelayerYieldRealizer = class {
1611
2178
  error
1612
2179
  );
1613
2180
  }
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) {
2181
+ const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
2182
+ if (spendable < requiredRawUsdc) {
1623
2183
  throw new RelayerRealizeError(
1624
2184
  "insufficient_yield",
1625
- `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
2185
+ `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
1626
2186
  { spendableYieldRawUsdc: spendable.toString() }
1627
2187
  );
1628
2188
  }
1629
2189
  }
1630
- /**
1631
- * Polls GET /v1/withdrawals/:id (which reconciles a submitted intent against
1632
- * the chain) until it leaves the "submitted" state or the timeout elapses.
1633
- */
1634
- async pollUntilSettled(withdrawalId, { timeoutMs = 9e4, intervalMs = 2500 } = {}) {
1635
- const deadline = Date.now() + timeoutMs;
1636
- let latest = null;
1637
- while (Date.now() < deadline) {
1638
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
1639
- const url = `${this.config.facilitatorBaseUrl}/v1/withdrawals/${withdrawalId}`;
1640
- const response = await this.fetchImpl(url, {
1641
- headers: await walletAuthHeaders({
1642
- signer: this.signer,
1643
- method: "GET",
1644
- url
1645
- })
1646
- });
1647
- if (response.status !== 200) {
1648
- continue;
1649
- }
1650
- latest = await response.json();
1651
- if (latest.status !== "submitted") {
1652
- return latest;
1653
- }
1654
- }
1655
- return latest ?? {
1656
- status: "submitted",
1657
- txSignature: null,
1658
- actualWithdrawRawUsdc: null
1659
- };
1660
- }
1661
- async postJson(path, body) {
1662
- const url = `${this.config.facilitatorBaseUrl}${path}`;
1663
- const serialized = JSON.stringify(body);
1664
- const response = await this.fetchImpl(url, {
1665
- method: "POST",
1666
- headers: {
1667
- ...await walletAuthHeaders({
1668
- signer: this.signer,
1669
- method: "POST",
1670
- url,
1671
- body: serialized
1672
- }),
1673
- "content-type": "application/json"
1674
- },
1675
- body: serialized
1676
- });
1677
- const text = await response.text();
1678
- if (response.status !== 200) {
1679
- throw new RelayerRealizeError(
1680
- path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
1681
- `${path} failed with ${response.status}: ${text}`
2190
+ mapWithdrawError(error) {
2191
+ if (!(error instanceof VaultFlowClientError)) {
2192
+ return new RelayerRealizeError(
2193
+ "prepare_failed",
2194
+ `yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
2195
+ error
1682
2196
  );
1683
2197
  }
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;
2198
+ const serverCode = errorCodeFrom(error.detail);
2199
+ if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
2200
+ return new RelayerRealizeError(
2201
+ "insufficient_yield",
2202
+ "the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
2203
+ error.detail
2204
+ );
1692
2205
  }
2206
+ return new RelayerRealizeError(
2207
+ error.step === "submit" ? "submit_failed" : "prepare_failed",
2208
+ error.message,
2209
+ error.detail
2210
+ );
1693
2211
  }
1694
2212
  };
2213
+ function errorCodeFrom(detail) {
2214
+ if (typeof detail !== "string") {
2215
+ return null;
2216
+ }
2217
+ try {
2218
+ const parsed = JSON.parse(detail);
2219
+ return typeof parsed.error?.code === "string" ? parsed.error.code : null;
2220
+ } catch {
2221
+ return null;
2222
+ }
2223
+ }
1695
2224
 
1696
2225
  // ../../src/client/relayer-payer.ts
1697
2226
  function createRelayerX402Payer(config) {
1698
2227
  const realizer = new RelayerYieldRealizer({
1699
- facilitatorBaseUrl: config.facilitatorBaseUrl,
2228
+ relayerBaseUrl: config.relayerBaseUrl,
1700
2229
  signer: config.signer,
1701
- rpc: config.rpc,
1702
- forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
2230
+ rpc: config.rpc
1703
2231
  });
1704
2232
  return new StandardX402Payer({
1705
2233
  realizer,
1706
2234
  x402Fetch: config.x402Fetch,
1707
- defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
2235
+ defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc,
2236
+ ...config.stateStore === void 0 ? {} : { stateStore: config.stateStore }
1708
2237
  });
1709
2238
  }
1710
2239
 
2240
+ // ../../src/client/standard-x402-state-store.ts
2241
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2242
+ import { basename, dirname, join } from "node:path";
2243
+ function fileStandardX402StateStore(path) {
2244
+ return {
2245
+ load() {
2246
+ let text;
2247
+ try {
2248
+ text = readFileSync(path, "utf8");
2249
+ } catch (error) {
2250
+ if (isMissingFileError(error)) {
2251
+ return [];
2252
+ }
2253
+ throw error;
2254
+ }
2255
+ const parsed = JSON.parse(text);
2256
+ if (!Array.isArray(parsed)) {
2257
+ throw new Error(`pending payment state is not an array: ${path}`);
2258
+ }
2259
+ for (const [index, record] of parsed.entries()) {
2260
+ if (!isPendingPaymentRecord(record)) {
2261
+ throw new Error(
2262
+ `pending payment state has an invalid record at index ${index}: ${path}`
2263
+ );
2264
+ }
2265
+ }
2266
+ return parsed;
2267
+ },
2268
+ save(records) {
2269
+ const directory = dirname(path);
2270
+ mkdirSync(directory, { recursive: true });
2271
+ const tempPath = join(
2272
+ directory,
2273
+ `.${basename(path)}.${process.pid}.${Date.now()}.tmp`
2274
+ );
2275
+ writeFileSync(tempPath, JSON.stringify(records, null, 2));
2276
+ renameSync(tempPath, path);
2277
+ }
2278
+ };
2279
+ }
2280
+ function isMissingFileError(error) {
2281
+ return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
2282
+ }
2283
+ function isPendingPaymentRecord(value) {
2284
+ if (value === null || typeof value !== "object") {
2285
+ return false;
2286
+ }
2287
+ const record = value;
2288
+ return typeof record.key === "string" && typeof record.url === "string" && typeof record.method === "string" && typeof record.requestBodyHash === "string" && typeof record.amountRawUsdc === "string" && typeof record.payTo === "string" && (record.feePayer === null || typeof record.feePayer === "string") && typeof record.realizedRawUsdc === "string" && (record.realizeTxSignature === null || typeof record.realizeTxSignature === "string") && (record.status === "realized" || record.status === "external_outcome_unknown") && typeof record.createdAtMs === "number" && typeof record.updatedAtMs === "number";
2289
+ }
2290
+
1711
2291
  // ../../src/solana/keys.ts
1712
- import { readFileSync } from "node:fs";
2292
+ import { readFileSync as readFileSync2 } from "node:fs";
1713
2293
  import bs586 from "bs58";
1714
2294
  import {
1715
2295
  createKeyPairSignerFromBytes
@@ -1724,7 +2304,7 @@ async function loadKeyPairSigner(params) {
1724
2304
  return createKeyPairSignerFromBytes(bytes);
1725
2305
  }
1726
2306
  if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1727
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
2307
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
1728
2308
  if (!Array.isArray(raw) || raw.length !== 64) {
1729
2309
  throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1730
2310
  }
@@ -1742,7 +2322,7 @@ function loadSecretKeyBytes(params) {
1742
2322
  return bytes;
1743
2323
  }
1744
2324
  if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1745
- const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
2325
+ const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
1746
2326
  if (!Array.isArray(raw) || raw.length !== 64) {
1747
2327
  throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1748
2328
  }
@@ -1765,21 +2345,36 @@ async function createSvmX402Fetch(params) {
1765
2345
  const signer2 = toClientSvmSigner(
1766
2346
  await createKeyPairSignerFromBytes2(params.agentSecretKey)
1767
2347
  );
1768
- const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
1769
- schemes: [
1770
- {
1771
- network: "solana:*",
1772
- client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
1773
- }
1774
- ]
1775
- });
1776
- return (url, init) => wrapped(url, init);
2348
+ return (url, init, expected) => {
2349
+ const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
2350
+ schemes: [
2351
+ {
2352
+ network: expected.requirement.network,
2353
+ client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
2354
+ }
2355
+ ],
2356
+ paymentRequirementsSelector: (_x402Version, requirements) => selectExpectedRequirement(requirements, expected)
2357
+ });
2358
+ return wrapped(url, init);
2359
+ };
2360
+ }
2361
+ function selectExpectedRequirement(requirements, expected) {
2362
+ const match = requirements.find(
2363
+ (candidate) => standardRequirementMatchesSelected(candidate, expected)
2364
+ );
2365
+ if (match === void 0) {
2366
+ throw new Error(
2367
+ "x402 challenge changed after preflight; refusing to pay an unchecked requirement"
2368
+ );
2369
+ }
2370
+ return match;
1777
2371
  }
1778
2372
 
1779
2373
  // src/mcp-server.ts
1780
- var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
2374
+ var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1781
2375
  var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
1782
2376
  var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
2377
+ var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? join2(homedir(), ".subly", "standard-x402-pending.json");
1783
2378
  var keyPairSigner = await loadKeyPairSigner({
1784
2379
  base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1785
2380
  jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
@@ -1793,15 +2388,21 @@ var agentSecretKey = loadSecretKeyBytes({
1793
2388
  var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1794
2389
  var rpc = createRpc(rpcUrl);
1795
2390
  var payer = createRelayerX402Payer({
1796
- facilitatorBaseUrl,
2391
+ relayerBaseUrl,
1797
2392
  signer,
1798
2393
  rpc,
1799
2394
  x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
1800
- defaultMaxAmountRawUsdc
2395
+ defaultMaxAmountRawUsdc,
2396
+ stateStore: fileStandardX402StateStore(pendingStatePath)
1801
2397
  });
1802
2398
  await runMcpPaymentServer({
1803
2399
  payer,
1804
2400
  signer,
1805
- facilitatorBaseUrl,
1806
- defaultMaxAmountRawUsdc
2401
+ relayerBaseUrl,
2402
+ defaultMaxAmountRawUsdc,
2403
+ vaultFlows: new VaultFlowClient({
2404
+ relayerBaseUrl,
2405
+ signer,
2406
+ rpc
2407
+ })
1807
2408
  });