@subly_fi/pay 0.4.0 → 0.5.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.
@@ -1006,7 +1006,7 @@ var OnboardingError = class extends Error {
1006
1006
  };
1007
1007
  async function ensureWalletOnboarded(params) {
1008
1008
  const fetchImpl = params.fetchImpl ?? fetch;
1009
- const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
1009
+ const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
1010
1010
  const post = async (step, path, body) => {
1011
1011
  const url = `${baseUrl}${path}`;
1012
1012
  const serialized = JSON.stringify(body);
@@ -1134,6 +1134,12 @@ function formatRawUsdcAmount(raw) {
1134
1134
  return `${negative ? "-" : ""}${whole}.${frac}`;
1135
1135
  }
1136
1136
 
1137
+ // ../../src/lib/canonical-json.ts
1138
+ import { createHash as createHash4 } from "node:crypto";
1139
+ function sha256HexOf(data) {
1140
+ return createHash4("sha256").update(data, "utf8").digest("hex");
1141
+ }
1142
+
1137
1143
  // ../../src/x402/standard-requirements.ts
1138
1144
  import { z as z2 } from "zod";
1139
1145
  var STANDARD_EXACT_SCHEME = "exact";
@@ -1199,20 +1205,36 @@ function decodeStandardPaymentRequiredHeader(headerValue) {
1199
1205
  function selectPayableSolanaRequirement(requirements, options) {
1200
1206
  const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1201
1207
  const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1202
- const requirement = requirements.find(
1208
+ const matchingRequirements = requirements.filter(
1203
1209
  (candidate) => candidate.network === network && candidate.asset === usdcMint
1204
- ) ?? null;
1205
- if (requirement === null) {
1210
+ );
1211
+ if (matchingRequirements.length === 0) {
1206
1212
  throw new StandardX402ChallengeError(
1207
1213
  "no_payable_requirement",
1208
1214
  `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1209
1215
  );
1210
1216
  }
1217
+ const requirement = matchingRequirements.find(
1218
+ (candidate) => candidate.extra?.feePayer !== void 0
1219
+ ) ?? null;
1220
+ if (requirement === null) {
1221
+ throw new StandardX402ChallengeError(
1222
+ "missing_svm_fee_payer",
1223
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1224
+ );
1225
+ }
1226
+ const feePayer = requirement.extra?.feePayer;
1227
+ if (feePayer === void 0) {
1228
+ throw new StandardX402ChallengeError(
1229
+ "missing_svm_fee_payer",
1230
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1231
+ );
1232
+ }
1211
1233
  return {
1212
1234
  requirement,
1213
1235
  amountRawUsdc: BigInt(requirement.amount),
1214
1236
  payTo: requirement.payTo,
1215
- feePayer: requirement.extra?.feePayer ?? null
1237
+ feePayer
1216
1238
  };
1217
1239
  }
1218
1240
  function standardRequirementMatchesSelected(candidate, selected) {
@@ -1335,9 +1357,23 @@ var StandardX402Payer = class {
1335
1357
  let realized;
1336
1358
  try {
1337
1359
  realized = await this.realizer.ensureUsdcAvailable({
1338
- amountRawUsdc: selected.amountRawUsdc
1360
+ amountRawUsdc: selected.amountRawUsdc,
1361
+ payment: {
1362
+ payTo: selected.payTo,
1363
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1364
+ resourceUrlHash: sha256HexOf(input.url),
1365
+ method
1366
+ },
1367
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1339
1368
  });
1340
1369
  } catch (error) {
1370
+ if (error.code === "approval_required") {
1371
+ throw new StandardX402PayError(
1372
+ "approval_required",
1373
+ "this payment exceeds the owner-approval threshold; NOTHING was paid. Ask the owner to open the approveUrl, then retry the same call with the approvalId",
1374
+ error.detail ?? null
1375
+ );
1376
+ }
1341
1377
  throw new StandardX402PayError(
1342
1378
  "realize_failed",
1343
1379
  `could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
@@ -1393,6 +1429,19 @@ var StandardX402Payer = class {
1393
1429
  );
1394
1430
  }
1395
1431
  this.clearDelivered(pendingKey);
1432
+ const paymentTxSignature = extractSettledPaymentTxSignature(response);
1433
+ if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && this.realizer.reportPayment !== void 0) {
1434
+ try {
1435
+ await this.realizer.reportPayment({
1436
+ withdrawalId: realized.withdrawalId,
1437
+ paymentTxSignature
1438
+ });
1439
+ } catch (error) {
1440
+ console.error(
1441
+ `[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
1442
+ );
1443
+ }
1444
+ }
1396
1445
  return {
1397
1446
  paid: true,
1398
1447
  status: response.status,
@@ -1402,7 +1451,8 @@ var StandardX402Payer = class {
1402
1451
  payTo: selected.payTo,
1403
1452
  feePayer: selected.feePayer,
1404
1453
  realizedRawUsdc: realized.realizedRawUsdc.toString(),
1405
- realizeTxSignature: realized.txSignature
1454
+ realizeTxSignature: realized.txSignature,
1455
+ paymentTxSignature
1406
1456
  }
1407
1457
  };
1408
1458
  }
@@ -1516,6 +1566,26 @@ var StandardX402Payer = class {
1516
1566
  function pendingPaymentKey(input) {
1517
1567
  return `${input.method}:${input.url}:${input.requestBodyHash}`;
1518
1568
  }
1569
+ function extractSettledPaymentTxSignature(response) {
1570
+ const header = response.headers.get("x-payment-response");
1571
+ if (header === null || header.length === 0) {
1572
+ return null;
1573
+ }
1574
+ try {
1575
+ const decoded = JSON.parse(
1576
+ Buffer.from(header, "base64").toString("utf8")
1577
+ );
1578
+ if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
1579
+ return decoded.transaction;
1580
+ }
1581
+ if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
1582
+ return decoded.txHash;
1583
+ }
1584
+ return null;
1585
+ } catch {
1586
+ return null;
1587
+ }
1588
+ }
1519
1589
 
1520
1590
  // ../../src/client/lookup-tables.ts
1521
1591
  import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
@@ -1559,14 +1629,18 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1559
1629
 
1560
1630
  // ../../src/client/vault-flows.ts
1561
1631
  var VaultFlowClientError = class extends Error {
1562
- constructor(step, message, detail = null) {
1632
+ constructor(step, message, detail = null, code = null, errorDetails = null) {
1563
1633
  super(message);
1564
1634
  this.step = step;
1565
1635
  this.detail = detail;
1636
+ this.code = code;
1637
+ this.errorDetails = errorDetails;
1566
1638
  this.name = "VaultFlowClientError";
1567
1639
  }
1568
1640
  step;
1569
1641
  detail;
1642
+ code;
1643
+ errorDetails;
1570
1644
  };
1571
1645
  var VaultFlowClient = class {
1572
1646
  baseUrl;
@@ -1576,19 +1650,44 @@ var VaultFlowClient = class {
1576
1650
  pollTimeoutMs;
1577
1651
  pollIntervalMs;
1578
1652
  constructor(config) {
1579
- this.baseUrl = config.facilitatorBaseUrl.replace(/\/$/, "");
1653
+ this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
1580
1654
  this.signer = config.signer;
1581
1655
  this.fetchImpl = config.fetchImpl ?? fetch;
1582
1656
  this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
1583
1657
  this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1584
1658
  this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1585
1659
  }
1586
- /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1660
+ /**
1661
+ * Moves USDC from the agent wallet into the vault (fee sponsored). Under
1662
+ * depositPolicy "owner_approval_required" the relayer refuses to prepare
1663
+ * without an owner approval; when the caller passes none, an already
1664
+ * APPROVED deposit approval for this exact amount (e.g. the mandate's
1665
+ * initialDeposit — "one Face ID covers mandate + first deposit") is looked
1666
+ * up and used automatically before surfacing deposit_approval_required.
1667
+ */
1587
1668
  async deposit(input) {
1588
- const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1589
- wallet: this.signer.walletAddress,
1590
- amountRawUsdc: input.amountRawUsdc.toString()
1591
- });
1669
+ let approvalId = input.approvalId;
1670
+ let prepared;
1671
+ try {
1672
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1673
+ wallet: this.signer.walletAddress,
1674
+ amountRawUsdc: input.amountRawUsdc.toString(),
1675
+ ...approvalId === void 0 ? {} : { approvalId }
1676
+ });
1677
+ } catch (error) {
1678
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
1679
+ throw error;
1680
+ }
1681
+ approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
1682
+ if (approvalId === void 0) {
1683
+ throw error;
1684
+ }
1685
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1686
+ wallet: this.signer.walletAddress,
1687
+ amountRawUsdc: input.amountRawUsdc.toString(),
1688
+ approvalId
1689
+ });
1690
+ }
1592
1691
  const signed = await this.signer.signDeposit({
1593
1692
  intent: prepared.signingIntent,
1594
1693
  serializedTransaction: prepared.serializedTransaction,
@@ -1627,7 +1726,9 @@ var VaultFlowClient = class {
1627
1726
  {
1628
1727
  wallet: this.signer.walletAddress,
1629
1728
  amountRawUsdc: input.amountRawUsdc.toString(),
1630
- ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1729
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1730
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1731
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1631
1732
  }
1632
1733
  );
1633
1734
  const signed = await this.signer.signWithdrawal({
@@ -1706,6 +1807,70 @@ var VaultFlowClient = class {
1706
1807
  spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1707
1808
  };
1708
1809
  }
1810
+ /** Best-effort audit link: reports the x402 payment tx a realize funded. */
1811
+ async reportPayment(input) {
1812
+ await this.postJson("submit", "/v1/payments/report", {
1813
+ wallet: this.signer.walletAddress,
1814
+ withdrawalId: input.withdrawalId,
1815
+ paymentTxSignature: input.paymentTxSignature
1816
+ });
1817
+ }
1818
+ /** Wallet's approvals as the relayer sees them (optionally by status). */
1819
+ async listApprovals(status) {
1820
+ const body = await this.getJson(
1821
+ `/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
1822
+ );
1823
+ return body.approvals ?? [];
1824
+ }
1825
+ /**
1826
+ * Creates the owner-onboarding setup link (wallet-auth pins the agreed
1827
+ * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1828
+ */
1829
+ async createSetupSession(input) {
1830
+ return await this.postJson(
1831
+ "prepare",
1832
+ `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1833
+ {
1834
+ ...input.policy === void 0 ? {} : { policy: input.policy },
1835
+ ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1836
+ ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1837
+ ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1838
+ }
1839
+ );
1840
+ }
1841
+ /** Polls a setup session (public capability URL — no auth needed). */
1842
+ async getSetupSession(sessionId) {
1843
+ const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
1844
+ const response = await this.fetchImpl(url);
1845
+ const text = await response.text();
1846
+ if (response.status !== 200) {
1847
+ const parsed = parseRelayerError(text);
1848
+ throw new VaultFlowClientError(
1849
+ "read",
1850
+ parsed.message ?? `setup session read failed with ${response.status}`,
1851
+ text,
1852
+ parsed.code,
1853
+ parsed.details
1854
+ );
1855
+ }
1856
+ return JSON.parse(text);
1857
+ }
1858
+ /**
1859
+ * Finds an APPROVED, unconsumed deposit approval bound to exactly this
1860
+ * amount — the shape the mandate's initialDeposit approval has.
1861
+ */
1862
+ async findApprovedDepositApproval(amountRawUsdc) {
1863
+ try {
1864
+ const approvals = await this.listApprovals("approved");
1865
+ const match = approvals.find((approval) => {
1866
+ const binding = approval.binding;
1867
+ return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
1868
+ });
1869
+ return match?.approvalId;
1870
+ } catch {
1871
+ return void 0;
1872
+ }
1873
+ }
1709
1874
  /**
1710
1875
  * Polls the reconciling GET endpoint until the intent leaves "submitted"
1711
1876
  * (each read looks the tx up on-chain) or the timeout elapses.
@@ -1755,10 +1920,13 @@ var VaultFlowClient = class {
1755
1920
  });
1756
1921
  const text = await response.text();
1757
1922
  if (response.status !== 200) {
1923
+ const parsed = parseRelayerError(text);
1758
1924
  throw new VaultFlowClientError(
1759
1925
  step,
1760
- `${path} failed with ${response.status}: ${text}`,
1761
- text
1926
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1927
+ text,
1928
+ parsed.code,
1929
+ parsed.details
1762
1930
  );
1763
1931
  }
1764
1932
  try {
@@ -1771,39 +1939,150 @@ var VaultFlowClient = class {
1771
1939
  );
1772
1940
  }
1773
1941
  }
1942
+ async getJson(path) {
1943
+ const url = `${this.baseUrl}${path}`;
1944
+ const response = await this.fetchImpl(url, {
1945
+ headers: await walletAuthHeaders({
1946
+ signer: this.signer,
1947
+ method: "GET",
1948
+ url
1949
+ })
1950
+ });
1951
+ const text = await response.text();
1952
+ if (response.status !== 200) {
1953
+ const parsed = parseRelayerError(text);
1954
+ throw new VaultFlowClientError(
1955
+ "read",
1956
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1957
+ text,
1958
+ parsed.code,
1959
+ parsed.details
1960
+ );
1961
+ }
1962
+ try {
1963
+ return JSON.parse(text);
1964
+ } catch {
1965
+ throw new VaultFlowClientError(
1966
+ "read",
1967
+ `${path} returned 200 with a non-JSON body`,
1968
+ text
1969
+ );
1970
+ }
1971
+ }
1774
1972
  };
1973
+ function parseRelayerError(text) {
1974
+ try {
1975
+ const parsed = JSON.parse(text);
1976
+ return {
1977
+ code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
1978
+ message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
1979
+ details: parsed.error?.details ?? null
1980
+ };
1981
+ } catch {
1982
+ return { code: null, message: null, details: null };
1983
+ }
1984
+ }
1775
1985
 
1776
1986
  // ../../src/client/mcp-payment-server.ts
1777
1987
  var TOOL_NAME = "fetch_with_subly_payment";
1778
1988
  var DEPOSIT_TOOL_NAME = "deposit_to_subly_vault";
1779
1989
  var WITHDRAW_TOOL_NAME = "withdraw_from_subly_vault";
1780
1990
  var BUDGET_TOOL_NAME = "get_subly_yield_budget";
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.
1991
+ var SETUP_TOOL_NAME = "create_subly_setup_link";
1992
+ var SETUP_STATUS_TOOL_NAME = "check_subly_setup";
1993
+ var SERVER_INSTRUCTIONS = `Subly lets an agent pay standard x402 (HTTP 402) paid APIs that offer a Solana USDC exact rail with facilitator feePayer support from its wallet's Kamino vault YIELD \u2014 the deposited principal is never spent, and the seller needs no Subly integration.
1782
1994
 
1783
1995
  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.
1784
1996
 
1997
+ Owner (human) onboarding: deposits require the human owner's approval (Face ID / wallet signature). During the first deposit conversation, agree the spending limits and the first deposit amount in chat, then call create_subly_setup_link and paste the returned setupUrl to the user AS IS (it expires in 10 minutes). The human opens it on their phone, reviews, and confirms once \u2014 that single confirmation activates the spending mandate AND pre-approves the first deposit. Poll check_subly_setup(sessionId) after the user says they finished, then call deposit_to_subly_vault (the pre-approved first deposit is picked up automatically).
1998
+
1785
1999
  From there the agent can do everything with these tools:
1786
- 1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (minimum 1 USDC = 1000000 raw) so it starts earning yield.
2000
+ 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. If it returns approvalRequired, paste the approveUrl to the user and retry with the approvalId after they approve; if it returns setupRequired, run the owner onboarding above first.
1787
2001
  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.`;
2002
+ 3. fetch_with_subly_payment(url) GETs or POSTs a paid resource from a compatible x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's Solana USDC exact 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. If it returns approvalRequired (payment above the owner's threshold; NOTHING was paid), paste the approveUrl to the user, and once they say they approved, repeat the SAME call adding the approvalId.
2003
+ 4. withdraw_from_subly_vault(amountRawUsdc) exits: moves vault funds (principal included) back to the agent wallet's USDC account. If the owner's mandate requires withdrawal approval it returns approvalRequired \u2014 same paste-approveUrl-then-retry flow as deposits.`;
1790
2004
  async function runMcpPaymentServer(config) {
1791
- const { payer: payer2, signer: signer2, facilitatorBaseUrl, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
2005
+ const { payer: payer2, signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
1792
2006
  const vaultFlows = config.vaultFlows ?? null;
1793
2007
  const server = new Server(
1794
2008
  { name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
1795
2009
  { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1796
2010
  );
1797
2011
  const vaultTools = vaultFlows === null ? [] : [
2012
+ {
2013
+ name: SETUP_TOOL_NAME,
2014
+ description: "Create the one-time owner setup link for this agent wallet's Subly spending mandate. Use during onboarding (the first deposit conversation): agree the limits and first deposit in chat, call this, and paste the returned setupUrl to the user verbatim \u2014 it expires in 10 minutes and works once. The human opens it on their phone and confirms with Face ID (passkey) or a Solana wallet signature; that single confirmation activates the mandate AND pre-approves the initial deposit. The page is confirm-only: to change values, agree in chat and create a new link.",
2015
+ inputSchema: {
2016
+ type: "object",
2017
+ properties: {
2018
+ initialDepositRawUsdc: {
2019
+ type: "string",
2020
+ description: `First deposit bundled into the owner's single Face ID (raw USDC, 6 decimals; just over 1 USDC minimum, e.g. "1010000"). Strongly recommended: without it the first deposit needs a separate approval.`
2021
+ },
2022
+ approvalThresholdRawUsdc: {
2023
+ type: "string",
2024
+ description: 'Payments at or below this run without asking (raw USDC). Default "1000000" (1 USDC).'
2025
+ },
2026
+ perPaymentCapRawUsdc: {
2027
+ type: "string",
2028
+ description: 'Absolute per-payment cap even with approval (raw USDC). Default "10000000" (10 USDC).'
2029
+ },
2030
+ dailyApiSpendCapRawUsdc: {
2031
+ type: "string",
2032
+ description: 'Rolling 24h API spend cap (raw USDC). Default "100000000" (100 USDC).'
2033
+ },
2034
+ dailyDepositCapRawUsdc: {
2035
+ type: "string",
2036
+ description: 'Rolling 24h deposit cap (raw USDC). Default "3000000000" (3,000 USDC).'
2037
+ },
2038
+ mandateTtlDays: {
2039
+ type: "number",
2040
+ description: "Mandate lifetime in days (default 365)."
2041
+ }
2042
+ }
2043
+ },
2044
+ annotations: {
2045
+ title: "Create Subly owner setup link",
2046
+ readOnlyHint: false,
2047
+ destructiveHint: false,
2048
+ idempotentHint: false,
2049
+ openWorldHint: false
2050
+ }
2051
+ },
2052
+ {
2053
+ name: SETUP_STATUS_TOOL_NAME,
2054
+ description: "Check whether the human completed a Subly setup link. Call after the user says they finished (or to verify before depositing). Returns pending / completed / expired; on completed it includes the mandateHash and, when an initial deposit was bundled, its pre-approved approvalId (valid ~15 minutes \u2014 deposit promptly).",
2055
+ inputSchema: {
2056
+ type: "object",
2057
+ properties: {
2058
+ sessionId: {
2059
+ type: "string",
2060
+ description: "The sessionId returned by create_subly_setup_link."
2061
+ }
2062
+ },
2063
+ required: ["sessionId"]
2064
+ },
2065
+ annotations: {
2066
+ title: "Check Subly setup status",
2067
+ readOnlyHint: true,
2068
+ destructiveHint: false,
2069
+ idempotentHint: true,
2070
+ openWorldHint: false
2071
+ }
2072
+ },
1798
2073
  {
1799
2074
  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 deposit is 1 USDC (1000000 raw). The deposited amount becomes protected principal: payments can only ever spend the yield on top of it.",
2075
+ 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. Deposits require the human owner's approval: a pre-approved amount (e.g. the setup link's initial deposit) is used automatically; otherwise the result contains an approveUrl \u2014 paste it to the user and retry with the approvalId once they approve. setupRequired means the owner onboarding (create_subly_setup_link) must happen first.",
1801
2076
  inputSchema: {
1802
2077
  type: "object",
1803
2078
  properties: {
1804
2079
  amountRawUsdc: {
1805
2080
  type: "string",
1806
- description: 'Amount to deposit in raw USDC units (6 decimals, e.g. "1000000" = 1 USDC). Must be at least 1000000.'
2081
+ 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.'
2082
+ },
2083
+ approvalId: {
2084
+ type: "string",
2085
+ description: "Owner approval id (apr_...) from a previous approvalRequired result or check_subly_setup, after the human approved."
1807
2086
  }
1808
2087
  },
1809
2088
  required: ["amountRawUsdc"]
@@ -1818,13 +2097,17 @@ async function runMcpPaymentServer(config) {
1818
2097
  },
1819
2098
  {
1820
2099
  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.",
2100
+ 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. If the owner's mandate requires withdrawal approval, the result contains an approveUrl \u2014 paste it to the user and retry with the approvalId once they approve.",
1822
2101
  inputSchema: {
1823
2102
  type: "object",
1824
2103
  properties: {
1825
2104
  amountRawUsdc: {
1826
2105
  type: "string",
1827
2106
  description: 'Amount to withdraw in raw USDC units (6 decimals, e.g. "1000000" = 1 USDC).'
2107
+ },
2108
+ approvalId: {
2109
+ type: "string",
2110
+ description: "Owner approval id (apr_...) from a previous approvalRequired result, after the human approved."
1828
2111
  }
1829
2112
  },
1830
2113
  required: ["amountRawUsdc"]
@@ -1855,7 +2138,7 @@ async function runMcpPaymentServer(config) {
1855
2138
  ...vaultTools,
1856
2139
  {
1857
2140
  name: TOOL_NAME,
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(
2141
+ description: `Fetch a URL (GET or POST), automatically paying a standard x402 (HTTP 402) challenge from a seller that offers Solana USDC \`exact\` with \`extra.feePayer\` (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 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(
1859
2142
  defaultMaxAmountRawUsdc2
1860
2143
  )} USDC) are refused without paying. Payments are refused when the spendable yield budget cannot cover them \u2014 the principal is never spent. Use only for URLs you intend to purchase access to.`,
1861
2144
  inputSchema: {
@@ -1885,6 +2168,10 @@ async function runMcpPaymentServer(config) {
1885
2168
  forceNewPayment: {
1886
2169
  type: "boolean",
1887
2170
  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."
2171
+ },
2172
+ approvalId: {
2173
+ type: "string",
2174
+ description: "Owner approval id (apr_...) from a previous approvalRequired result. After the human approves via the approveUrl, repeat the SAME call with this added."
1888
2175
  }
1889
2176
  },
1890
2177
  required: ["url"]
@@ -1954,13 +2241,19 @@ async function runMcpPaymentServer(config) {
1954
2241
  const vaultToolNames = [
1955
2242
  BUDGET_TOOL_NAME,
1956
2243
  DEPOSIT_TOOL_NAME,
1957
- WITHDRAW_TOOL_NAME
2244
+ WITHDRAW_TOOL_NAME,
2245
+ SETUP_TOOL_NAME,
2246
+ SETUP_STATUS_TOOL_NAME
1958
2247
  ];
1959
2248
  if (vaultFlows !== null && vaultToolNames.includes(request.params.name)) {
1960
- try {
1961
- await ensureWalletOnboarded({ facilitatorBaseUrl, signer: signer2 });
1962
- } catch {
2249
+ const needsChainSync = request.params.name !== SETUP_TOOL_NAME && request.params.name !== SETUP_STATUS_TOOL_NAME;
2250
+ if (needsChainSync) {
2251
+ try {
2252
+ await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
2253
+ } catch {
2254
+ }
1963
2255
  }
2256
+ const args2 = request.params.arguments ?? {};
1964
2257
  if (request.params.name === BUDGET_TOOL_NAME) {
1965
2258
  try {
1966
2259
  const budget = await vaultFlows.getBudget();
@@ -1980,9 +2273,48 @@ async function runMcpPaymentServer(config) {
1980
2273
  return vaultFlowFailure(error);
1981
2274
  }
1982
2275
  }
1983
- const amountRawUsdc = parseRawAmount(
1984
- (request.params.arguments ?? {}).amountRawUsdc
1985
- );
2276
+ if (request.params.name === SETUP_TOOL_NAME) {
2277
+ try {
2278
+ const policy = {};
2279
+ for (const key of [
2280
+ "approvalThresholdRawUsdc",
2281
+ "perPaymentCapRawUsdc",
2282
+ "dailyApiSpendCapRawUsdc",
2283
+ "dailyDepositCapRawUsdc"
2284
+ ]) {
2285
+ const value = args2[key];
2286
+ if (typeof value === "string" && value.length > 0) {
2287
+ policy[key] = value;
2288
+ }
2289
+ }
2290
+ const created = await vaultFlows.createSetupSession({
2291
+ ...Object.keys(policy).length === 0 ? {} : { policy },
2292
+ ...typeof args2.mandateTtlDays === "number" ? { mandateTtlDays: args2.mandateTtlDays } : {},
2293
+ ...typeof args2.initialDepositRawUsdc === "string" ? { initialDepositRawUsdc: args2.initialDepositRawUsdc } : {}
2294
+ });
2295
+ return textResult({
2296
+ ...created,
2297
+ instructions: `Paste setupUrl to the user verbatim (expires in 10 minutes, single-use). After they confirm on their device, call ${SETUP_STATUS_TOOL_NAME} with this sessionId; when completed, run the first deposit \u2014 its approval is picked up automatically.`
2298
+ });
2299
+ } catch (error) {
2300
+ return vaultFlowFailure(error);
2301
+ }
2302
+ }
2303
+ if (request.params.name === SETUP_STATUS_TOOL_NAME) {
2304
+ const sessionId = args2.sessionId;
2305
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
2306
+ return textResult(
2307
+ { ok: false, message: "missing required argument: sessionId" },
2308
+ true
2309
+ );
2310
+ }
2311
+ try {
2312
+ return textResult(await vaultFlows.getSetupSession(sessionId));
2313
+ } catch (error) {
2314
+ return vaultFlowFailure(error);
2315
+ }
2316
+ }
2317
+ const amountRawUsdc = parseRawAmount(args2.amountRawUsdc);
1986
2318
  if (amountRawUsdc === null) {
1987
2319
  return textResult(
1988
2320
  {
@@ -1992,22 +2324,50 @@ async function runMcpPaymentServer(config) {
1992
2324
  true
1993
2325
  );
1994
2326
  }
2327
+ const flowApprovalId = typeof args2.approvalId === "string" && args2.approvalId.length > 0 ? args2.approvalId : void 0;
1995
2328
  try {
1996
2329
  if (request.params.name === DEPOSIT_TOOL_NAME) {
1997
- const outcome2 = await vaultFlows.deposit({ amountRawUsdc });
2330
+ const outcome2 = await vaultFlows.deposit({
2331
+ amountRawUsdc,
2332
+ ...flowApprovalId === void 0 ? {} : { approvalId: flowApprovalId }
2333
+ });
1998
2334
  return vaultFlowOutcome(outcome2, {
1999
2335
  depositedUsdc: formatRawUsdcAmount(
2000
2336
  BigInt(outcome2.actualDepositRawUsdc ?? "0")
2001
2337
  )
2002
2338
  });
2003
2339
  }
2004
- const outcome = await vaultFlows.withdraw({ amountRawUsdc });
2340
+ const outcome = await vaultFlows.withdraw({
2341
+ amountRawUsdc,
2342
+ ...flowApprovalId === void 0 ? {} : { approvalId: flowApprovalId }
2343
+ });
2005
2344
  return vaultFlowOutcome(outcome, {
2006
2345
  withdrawnUsdc: formatRawUsdcAmount(
2007
2346
  BigInt(outcome.actualWithdrawRawUsdc ?? "0")
2008
2347
  )
2009
2348
  });
2010
2349
  } catch (error) {
2350
+ if (error instanceof VaultFlowClientError) {
2351
+ const details = error.errorDetails ?? {};
2352
+ if (error.code === "deposit_approval_required" || error.code === "withdrawal_approval_required") {
2353
+ const op = error.code === "deposit_approval_required" ? "deposit" : "withdrawal";
2354
+ return textResult({
2355
+ ok: false,
2356
+ approvalRequired: true,
2357
+ approvalId: details.approvalId ?? null,
2358
+ approveUrl: details.approveUrl ?? null,
2359
+ expiresAtMs: details.expiresAtMs ?? null,
2360
+ message: `This ${op} needs the owner's approval. Paste approveUrl to the user; once they approve (Face ID / wallet sign), retry the same ${op} adding the approvalId.`
2361
+ });
2362
+ }
2363
+ if (error.code === "mandate_required_for_deposit") {
2364
+ return textResult({
2365
+ ok: false,
2366
+ setupRequired: true,
2367
+ message: `Deposits require a registered owner. Run the onboarding: agree limits + first deposit in chat, call ${SETUP_TOOL_NAME}, and paste the setupUrl to the user. The setup's initial deposit is pre-approved with the same single Face ID.`
2368
+ });
2369
+ }
2370
+ }
2011
2371
  return vaultFlowFailure(error);
2012
2372
  }
2013
2373
  }
@@ -2048,6 +2408,7 @@ async function runMcpPaymentServer(config) {
2048
2408
  const method = typeof args.method === "string" ? args.method : void 0;
2049
2409
  const body = typeof args.body === "string" ? args.body : void 0;
2050
2410
  const forceNewPayment = args.forceNewPayment === true;
2411
+ const approvalId = typeof args.approvalId === "string" && args.approvalId.length > 0 ? args.approvalId : void 0;
2051
2412
  const headers = args.headers !== null && typeof args.headers === "object" && !Array.isArray(args.headers) ? Object.fromEntries(
2052
2413
  Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
2053
2414
  ) : void 0;
@@ -2059,12 +2420,24 @@ async function runMcpPaymentServer(config) {
2059
2420
  ...body === void 0 ? {} : { body },
2060
2421
  ...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
2061
2422
  ...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
2062
- ...forceNewPayment ? { forceNewPayment } : {}
2423
+ ...forceNewPayment ? { forceNewPayment } : {},
2424
+ ...approvalId === void 0 ? {} : { approvalId }
2063
2425
  });
2064
2426
  return {
2065
2427
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2066
2428
  };
2067
2429
  } catch (error) {
2430
+ if (error instanceof StandardX402PayError && error.reason === "approval_required") {
2431
+ const details = error.detail ?? {};
2432
+ return textResult({
2433
+ paid: false,
2434
+ approvalRequired: true,
2435
+ approvalId: details.approvalId ?? null,
2436
+ approveUrl: details.approveUrl ?? null,
2437
+ expiresAtMs: details.expiresAtMs ?? null,
2438
+ message: "This payment exceeds the owner's approval threshold \u2014 NOTHING was paid. Paste approveUrl to the user; once they approve, repeat the SAME call adding the approvalId."
2439
+ });
2440
+ }
2068
2441
  if (error instanceof StandardX402PayError) {
2069
2442
  return {
2070
2443
  content: [
@@ -2098,7 +2471,7 @@ async function runMcpPaymentServer(config) {
2098
2471
  }
2099
2472
  });
2100
2473
  try {
2101
- await ensureWalletOnboarded({ facilitatorBaseUrl, signer: signer2 });
2474
+ await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
2102
2475
  console.error("[subly-mcp] wallet registered and synced at the relayer");
2103
2476
  } catch (error) {
2104
2477
  console.error(
@@ -2108,7 +2481,7 @@ async function runMcpPaymentServer(config) {
2108
2481
  const transport = new StdioServerTransport();
2109
2482
  await server.connect(transport);
2110
2483
  console.error(
2111
- `[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
2484
+ `[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${relayerBaseUrl2}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
2112
2485
  );
2113
2486
  }
2114
2487
 
@@ -2128,7 +2501,7 @@ var RelayerYieldRealizer = class {
2128
2501
  vaultFlows;
2129
2502
  constructor(config) {
2130
2503
  this.vaultFlows = new VaultFlowClient({
2131
- facilitatorBaseUrl: config.facilitatorBaseUrl,
2504
+ relayerBaseUrl: config.relayerBaseUrl,
2132
2505
  signer: config.signer,
2133
2506
  rpc: config.rpc,
2134
2507
  ...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
@@ -2144,7 +2517,11 @@ var RelayerYieldRealizer = class {
2144
2517
  amountRawUsdc: shortfallRawUsdc,
2145
2518
  // The relayer refuses to prepare this withdrawal beyond the spendable
2146
2519
  // yield — the principal-protection guard the client cannot bypass.
2147
- purpose: "yield_realize"
2520
+ purpose: "yield_realize",
2521
+ // Declares what is being paid so the relayer's spending-mandate layer
2522
+ // can enforce caps/payee and keep the mandate → payment audit chain.
2523
+ ...input.payment === void 0 ? {} : { payment: input.payment },
2524
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
2148
2525
  });
2149
2526
  } catch (error) {
2150
2527
  throw this.mapWithdrawError(error);
@@ -2158,9 +2535,18 @@ var RelayerYieldRealizer = class {
2158
2535
  }
2159
2536
  return {
2160
2537
  realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
2161
- txSignature: outcome.txSignature
2538
+ txSignature: outcome.txSignature,
2539
+ withdrawalId: outcome.withdrawalId
2162
2540
  };
2163
2541
  }
2542
+ /**
2543
+ * Best-effort report-back of the x402 payment tx this realize funded —
2544
+ * closes the relayer's mandate → realize → payment audit chain. Callers
2545
+ * must never let a failure here affect the payment result.
2546
+ */
2547
+ async reportPayment(input) {
2548
+ await this.vaultFlows.reportPayment(input);
2549
+ }
2164
2550
  /**
2165
2551
  * Refuses to realize more than the ledger's spendable yield (principal).
2166
2552
  * getBudget syncs the relayer's ledger from chain first (best-effort), so a
@@ -2195,7 +2581,14 @@ var RelayerYieldRealizer = class {
2195
2581
  error
2196
2582
  );
2197
2583
  }
2198
- const serverCode = errorCodeFrom(error.detail);
2584
+ const serverCode = error.code ?? errorCodeFrom(error.detail);
2585
+ if (serverCode === "approval_required") {
2586
+ return new RelayerRealizeError(
2587
+ "approval_required",
2588
+ "this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
2589
+ error.errorDetails ?? error.detail
2590
+ );
2591
+ }
2199
2592
  if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
2200
2593
  return new RelayerRealizeError(
2201
2594
  "insufficient_yield",
@@ -2225,7 +2618,7 @@ function errorCodeFrom(detail) {
2225
2618
  // ../../src/client/relayer-payer.ts
2226
2619
  function createRelayerX402Payer(config) {
2227
2620
  const realizer = new RelayerYieldRealizer({
2228
- facilitatorBaseUrl: config.facilitatorBaseUrl,
2621
+ relayerBaseUrl: config.relayerBaseUrl,
2229
2622
  signer: config.signer,
2230
2623
  rpc: config.rpc
2231
2624
  });
@@ -2388,7 +2781,7 @@ var agentSecretKey = loadSecretKeyBytes({
2388
2781
  var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
2389
2782
  var rpc = createRpc(rpcUrl);
2390
2783
  var payer = createRelayerX402Payer({
2391
- facilitatorBaseUrl: relayerBaseUrl,
2784
+ relayerBaseUrl,
2392
2785
  signer,
2393
2786
  rpc,
2394
2787
  x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
@@ -2398,10 +2791,10 @@ var payer = createRelayerX402Payer({
2398
2791
  await runMcpPaymentServer({
2399
2792
  payer,
2400
2793
  signer,
2401
- facilitatorBaseUrl: relayerBaseUrl,
2794
+ relayerBaseUrl,
2402
2795
  defaultMaxAmountRawUsdc,
2403
2796
  vaultFlows: new VaultFlowClient({
2404
- facilitatorBaseUrl: relayerBaseUrl,
2797
+ relayerBaseUrl,
2405
2798
  signer,
2406
2799
  rpc
2407
2800
  })