@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.
- package/README.md +13 -5
- package/dist/deposit.js +153 -13
- package/dist/mcp-server.js +439 -46
- package/dist/pay.js +254 -24
- package/dist/withdraw.js +151 -11
- package/package.json +2 -2
package/dist/mcp-server.js
CHANGED
|
@@ -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.
|
|
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
|
|
1208
|
+
const matchingRequirements = requirements.filter(
|
|
1203
1209
|
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1204
|
-
)
|
|
1205
|
-
if (
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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. "
|
|
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
|
|
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
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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
|
-
|
|
1984
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2794
|
+
relayerBaseUrl,
|
|
2402
2795
|
defaultMaxAmountRawUsdc,
|
|
2403
2796
|
vaultFlows: new VaultFlowClient({
|
|
2404
|
-
|
|
2797
|
+
relayerBaseUrl,
|
|
2405
2798
|
signer,
|
|
2406
2799
|
rpc
|
|
2407
2800
|
})
|