@subly_fi/pay 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deposit.js +18 -18
- package/dist/mcp-server.js +442 -378
- package/dist/pay.js +472 -572
- package/dist/withdraw.js +17 -17
- package/package.json +9 -7
package/dist/mcp-server.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
-
CallToolRequestSchema,
|
|
6
|
-
ListToolsRequestSchema
|
|
7
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
|
|
9
|
-
// src/client/agent-wallet-signer.ts
|
|
1
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
10
2
|
import { signBytes } from "@solana/kit";
|
|
11
3
|
import bs584 from "bs58";
|
|
12
4
|
|
|
13
|
-
// src/solana/tx.ts
|
|
5
|
+
// ../../src/solana/tx.ts
|
|
14
6
|
import bs58 from "bs58";
|
|
15
7
|
import {
|
|
16
8
|
appendTransactionMessageInstructions,
|
|
@@ -25,7 +17,7 @@ import {
|
|
|
25
17
|
setTransactionMessageLifetimeUsingBlockhash
|
|
26
18
|
} from "@solana/kit";
|
|
27
19
|
|
|
28
|
-
// src/lib/hash.ts
|
|
20
|
+
// ../../src/lib/hash.ts
|
|
29
21
|
import { createHash } from "node:crypto";
|
|
30
22
|
function sha256TaggedHex(data) {
|
|
31
23
|
return `sha256-${createHash("sha256").update(data).digest("hex")}`;
|
|
@@ -50,7 +42,7 @@ function hashStableJson(value) {
|
|
|
50
42
|
return sha256TaggedHex(stableStringify(value));
|
|
51
43
|
}
|
|
52
44
|
|
|
53
|
-
// src/solana/tx.ts
|
|
45
|
+
// ../../src/solana/tx.ts
|
|
54
46
|
function decodeSerializedTransaction(serializedBase64) {
|
|
55
47
|
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
56
48
|
}
|
|
@@ -70,11 +62,11 @@ function signatureBase58ForSigner(transaction, signer2) {
|
|
|
70
62
|
return bs58.encode(signature);
|
|
71
63
|
}
|
|
72
64
|
|
|
73
|
-
// src/client/transaction-intent-validator.ts
|
|
65
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
74
66
|
import bs583 from "bs58";
|
|
75
67
|
import { getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
76
68
|
|
|
77
|
-
// src/config/constants.ts
|
|
69
|
+
// ../../src/config/constants.ts
|
|
78
70
|
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
79
71
|
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
80
72
|
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
@@ -90,7 +82,7 @@ var SUBLY_VAULT = {
|
|
|
90
82
|
};
|
|
91
83
|
var USDC_DECIMALS = 6;
|
|
92
84
|
|
|
93
|
-
// src/domain/request-binding.ts
|
|
85
|
+
// ../../src/domain/request-binding.ts
|
|
94
86
|
function computeRequestBindingHash(fields) {
|
|
95
87
|
return hashStableJson({
|
|
96
88
|
sellerRequestId: fields.sellerRequestId,
|
|
@@ -105,7 +97,7 @@ function computeRequestBindingHash(fields) {
|
|
|
105
97
|
});
|
|
106
98
|
}
|
|
107
99
|
|
|
108
|
-
// src/lib/associated-token-account.ts
|
|
100
|
+
// ../../src/lib/associated-token-account.ts
|
|
109
101
|
import { createHash as createHash2 } from "node:crypto";
|
|
110
102
|
import bs582 from "bs58";
|
|
111
103
|
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
@@ -197,7 +189,7 @@ function modPow(base, exponent, modulus) {
|
|
|
197
189
|
return result;
|
|
198
190
|
}
|
|
199
191
|
|
|
200
|
-
// src/client/transaction-intent-validator.ts
|
|
192
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
201
193
|
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
202
194
|
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
203
195
|
var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
@@ -883,7 +875,7 @@ function readShortVec(bytes, startOffset) {
|
|
|
883
875
|
return null;
|
|
884
876
|
}
|
|
885
877
|
|
|
886
|
-
// src/client/agent-wallet-signer.ts
|
|
878
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
887
879
|
var LocalKeypairAgentWalletSigner = class {
|
|
888
880
|
validationMode = "structured_intent_transaction";
|
|
889
881
|
keyPairSigner;
|
|
@@ -953,7 +945,15 @@ var LocalKeypairAgentWalletSigner = class {
|
|
|
953
945
|
}
|
|
954
946
|
};
|
|
955
947
|
|
|
956
|
-
// src/
|
|
948
|
+
// ../../src/client/mcp-payment-server.ts
|
|
949
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
950
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
951
|
+
import {
|
|
952
|
+
CallToolRequestSchema,
|
|
953
|
+
ListToolsRequestSchema
|
|
954
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
955
|
+
|
|
956
|
+
// ../../src/api/wallet-auth.ts
|
|
957
957
|
import { createHash as createHash3 } from "node:crypto";
|
|
958
958
|
import bs585 from "bs58";
|
|
959
959
|
import nacl from "tweetnacl";
|
|
@@ -971,7 +971,7 @@ function walletAuthMessage(params) {
|
|
|
971
971
|
);
|
|
972
972
|
}
|
|
973
973
|
|
|
974
|
-
// src/client/wallet-auth-headers.ts
|
|
974
|
+
// ../../src/client/wallet-auth-headers.ts
|
|
975
975
|
async function walletAuthHeaders(params) {
|
|
976
976
|
const signedAtMs = String(Date.now());
|
|
977
977
|
const message = walletAuthMessage({
|
|
@@ -987,7 +987,7 @@ async function walletAuthHeaders(params) {
|
|
|
987
987
|
};
|
|
988
988
|
}
|
|
989
989
|
|
|
990
|
-
// src/client/onboarding.ts
|
|
990
|
+
// ../../src/client/onboarding.ts
|
|
991
991
|
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
992
992
|
var OnboardingError = class extends Error {
|
|
993
993
|
constructor(step, message, detail = null) {
|
|
@@ -1044,187 +1044,7 @@ async function ensureWalletOnboarded(params) {
|
|
|
1044
1044
|
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
1045
1045
|
}
|
|
1046
1046
|
|
|
1047
|
-
// src/
|
|
1048
|
-
import { createX402Client } from "x402-solana/client";
|
|
1049
|
-
|
|
1050
|
-
// src/client/relayer-yield-realizer.ts
|
|
1051
|
-
import { address as address2 } from "@solana/kit";
|
|
1052
|
-
|
|
1053
|
-
// src/client/lookup-tables.ts
|
|
1054
|
-
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1055
|
-
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1056
|
-
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
1057
|
-
const wire = Buffer.from(serializedTransaction, "base64");
|
|
1058
|
-
let offset = 0;
|
|
1059
|
-
let signatureCount = 0;
|
|
1060
|
-
let shift = 0;
|
|
1061
|
-
while (offset < wire.length) {
|
|
1062
|
-
const byte = wire[offset];
|
|
1063
|
-
signatureCount |= (byte & 127) << shift;
|
|
1064
|
-
offset += 1;
|
|
1065
|
-
if ((byte & 128) === 0) {
|
|
1066
|
-
break;
|
|
1067
|
-
}
|
|
1068
|
-
shift += 7;
|
|
1069
|
-
}
|
|
1070
|
-
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
1071
|
-
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
1072
|
-
const lookups = compiled.addressTableLookups ?? [];
|
|
1073
|
-
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
1074
|
-
}
|
|
1075
|
-
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
1076
|
-
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
1077
|
-
if (addresses.length === 0) {
|
|
1078
|
-
return {};
|
|
1079
|
-
}
|
|
1080
|
-
const tables = await fetchAllMaybeAddressLookupTable(
|
|
1081
|
-
rpc2,
|
|
1082
|
-
addresses.map((value) => address(value))
|
|
1083
|
-
);
|
|
1084
|
-
const result = {};
|
|
1085
|
-
for (const table of tables) {
|
|
1086
|
-
if (table.exists) {
|
|
1087
|
-
result[table.address] = table.data.addresses.map(String);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
return result;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// src/client/relayer-yield-realizer.ts
|
|
1094
|
-
var RelayerRealizeError = class extends Error {
|
|
1095
|
-
constructor(code, message, detail = null) {
|
|
1096
|
-
super(message);
|
|
1097
|
-
this.code = code;
|
|
1098
|
-
this.detail = detail;
|
|
1099
|
-
this.name = "RelayerRealizeError";
|
|
1100
|
-
}
|
|
1101
|
-
code;
|
|
1102
|
-
detail;
|
|
1103
|
-
};
|
|
1104
|
-
var RelayerYieldRealizer = class {
|
|
1105
|
-
config;
|
|
1106
|
-
signer;
|
|
1107
|
-
rpc;
|
|
1108
|
-
fetchImpl;
|
|
1109
|
-
lookupTablesFor;
|
|
1110
|
-
constructor(config) {
|
|
1111
|
-
this.config = {
|
|
1112
|
-
facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
|
|
1113
|
-
usdcMint: config.usdcMint ?? SUBLY_VAULT.usdcMint
|
|
1114
|
-
};
|
|
1115
|
-
this.signer = config.signer;
|
|
1116
|
-
this.rpc = config.rpc;
|
|
1117
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1118
|
-
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
|
|
1119
|
-
}
|
|
1120
|
-
async ensureUsdcAvailable(input) {
|
|
1121
|
-
const agentAta = deriveAssociatedTokenAddress({
|
|
1122
|
-
owner: this.signer.walletAddress,
|
|
1123
|
-
mint: this.config.usdcMint
|
|
1124
|
-
});
|
|
1125
|
-
const currentBalance = await this.readTokenBalance(agentAta);
|
|
1126
|
-
if (currentBalance >= input.amountRawUsdc) {
|
|
1127
|
-
return { realizedRawUsdc: 0n, txSignature: null };
|
|
1128
|
-
}
|
|
1129
|
-
const shortfallRawUsdc = input.amountRawUsdc - currentBalance;
|
|
1130
|
-
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1131
|
-
const prepared = await this.postJson("/v1/withdrawals/prepare", {
|
|
1132
|
-
wallet: this.signer.walletAddress,
|
|
1133
|
-
amountRawUsdc: shortfallRawUsdc.toString()
|
|
1134
|
-
});
|
|
1135
|
-
const signed = await this.signer.signWithdrawal({
|
|
1136
|
-
intent: prepared.signingIntent,
|
|
1137
|
-
serializedTransaction: prepared.serializedTransaction,
|
|
1138
|
-
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1139
|
-
});
|
|
1140
|
-
const submitted = await this.postJson("/v1/withdrawals/submit", {
|
|
1141
|
-
withdrawalId: prepared.withdrawalId,
|
|
1142
|
-
serializedTransaction: signed.serializedTransaction,
|
|
1143
|
-
agentSignature: signed.agentSignature
|
|
1144
|
-
});
|
|
1145
|
-
if (submitted.status !== "confirmed" || submitted.txSignature === null) {
|
|
1146
|
-
throw new RelayerRealizeError(
|
|
1147
|
-
"realize_not_confirmed",
|
|
1148
|
-
`yield realize withdrawal did not confirm (status=${submitted.status})`,
|
|
1149
|
-
submitted
|
|
1150
|
-
);
|
|
1151
|
-
}
|
|
1152
|
-
return {
|
|
1153
|
-
realizedRawUsdc: BigInt(submitted.actualWithdrawRawUsdc ?? "0"),
|
|
1154
|
-
txSignature: submitted.txSignature
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
/** Refuses to realize more than the ledger's spendable yield (principal). */
|
|
1158
|
-
async assertSpendableYield(shortfallRawUsdc) {
|
|
1159
|
-
const url = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1160
|
-
let response;
|
|
1161
|
-
try {
|
|
1162
|
-
response = await this.fetchImpl(url, {
|
|
1163
|
-
headers: await walletAuthHeaders({
|
|
1164
|
-
signer: this.signer,
|
|
1165
|
-
method: "GET",
|
|
1166
|
-
url
|
|
1167
|
-
})
|
|
1168
|
-
});
|
|
1169
|
-
} catch (error) {
|
|
1170
|
-
throw new RelayerRealizeError(
|
|
1171
|
-
"budget_unavailable",
|
|
1172
|
-
"could not read the spendable-yield budget",
|
|
1173
|
-
error
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
if (response.status !== 200) {
|
|
1177
|
-
throw new RelayerRealizeError(
|
|
1178
|
-
"budget_unavailable",
|
|
1179
|
-
`budget endpoint returned ${response.status}`
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
const body = await response.json();
|
|
1183
|
-
const spendable = BigInt(body.budget?.spendableYieldRawUsdc ?? "0");
|
|
1184
|
-
if (spendable < shortfallRawUsdc) {
|
|
1185
|
-
throw new RelayerRealizeError(
|
|
1186
|
-
"insufficient_yield",
|
|
1187
|
-
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
|
|
1188
|
-
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
async postJson(path, body) {
|
|
1193
|
-
const url = `${this.config.facilitatorBaseUrl}${path}`;
|
|
1194
|
-
const serialized = JSON.stringify(body);
|
|
1195
|
-
const response = await this.fetchImpl(url, {
|
|
1196
|
-
method: "POST",
|
|
1197
|
-
headers: {
|
|
1198
|
-
...await walletAuthHeaders({
|
|
1199
|
-
signer: this.signer,
|
|
1200
|
-
method: "POST",
|
|
1201
|
-
url,
|
|
1202
|
-
body: serialized
|
|
1203
|
-
}),
|
|
1204
|
-
"content-type": "application/json"
|
|
1205
|
-
},
|
|
1206
|
-
body: serialized
|
|
1207
|
-
});
|
|
1208
|
-
const text = await response.text();
|
|
1209
|
-
if (response.status !== 200) {
|
|
1210
|
-
throw new RelayerRealizeError(
|
|
1211
|
-
path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
|
|
1212
|
-
`${path} failed with ${response.status}: ${text}`
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1215
|
-
return JSON.parse(text);
|
|
1216
|
-
}
|
|
1217
|
-
async readTokenBalance(ata) {
|
|
1218
|
-
try {
|
|
1219
|
-
const response = await this.rpc.getTokenAccountBalance(address2(ata), { commitment: "confirmed" }).send();
|
|
1220
|
-
return BigInt(response.value.amount);
|
|
1221
|
-
} catch {
|
|
1222
|
-
return 0n;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
};
|
|
1226
|
-
|
|
1227
|
-
// src/x402/headers.ts
|
|
1047
|
+
// ../../src/x402/headers.ts
|
|
1228
1048
|
import { z } from "zod";
|
|
1229
1049
|
var PAYMENT_REQUIRED_HEADER = "payment-required";
|
|
1230
1050
|
var MAX_HEADER_JSON_BYTES = 16384;
|
|
@@ -1291,7 +1111,17 @@ function decodeX402Header(headerValue) {
|
|
|
1291
1111
|
}
|
|
1292
1112
|
}
|
|
1293
1113
|
|
|
1294
|
-
// src/
|
|
1114
|
+
// ../../src/client/paid-fetch.ts
|
|
1115
|
+
function formatRawUsdcAmount(raw) {
|
|
1116
|
+
const value = BigInt(raw);
|
|
1117
|
+
const negative = value < 0n;
|
|
1118
|
+
const abs = negative ? -value : value;
|
|
1119
|
+
const whole = abs / 1000000n;
|
|
1120
|
+
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
1121
|
+
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ../../src/x402/standard-requirements.ts
|
|
1295
1125
|
import { z as z2 } from "zod";
|
|
1296
1126
|
var STANDARD_EXACT_SCHEME = "exact";
|
|
1297
1127
|
var standardExactRequirementSchema = z2.object({
|
|
@@ -1373,7 +1203,7 @@ function selectPayableSolanaRequirement(requirements, options) {
|
|
|
1373
1203
|
};
|
|
1374
1204
|
}
|
|
1375
1205
|
|
|
1376
|
-
// src/client/standard-x402-payer.ts
|
|
1206
|
+
// ../../src/client/standard-x402-payer.ts
|
|
1377
1207
|
var StandardX402PayError = class extends Error {
|
|
1378
1208
|
constructor(reason, message, detail = null) {
|
|
1379
1209
|
super(message);
|
|
@@ -1486,53 +1316,399 @@ var StandardX402Payer = class {
|
|
|
1486
1316
|
}
|
|
1487
1317
|
};
|
|
1488
1318
|
|
|
1489
|
-
// src/client/
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1319
|
+
// ../../src/client/mcp-payment-server.ts
|
|
1320
|
+
var TOOL_NAME = "fetch_with_subly_payment";
|
|
1321
|
+
var SERVER_INSTRUCTIONS = `Subly lets an agent pay for ANY standard x402 (HTTP 402) paid API from its wallet's Kamino vault YIELD \u2014 the deposited principal is never spent, and the seller needs no Subly integration.
|
|
1322
|
+
|
|
1323
|
+
Before payments can succeed the operator of this server must have, once:
|
|
1324
|
+
1. A Solana keypair for the agent wallet. Subly does NOT create wallets; make one with \`solana-keygen new -o agent.json\` (or export a keypair from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it. The private key never leaves that file; this server only signs locally with it.
|
|
1325
|
+
2. Funded that wallet with USDC on Solana mainnet (no SOL needed \u2014 realize fees are sponsored) and deposited into the vault (see the project's deposit command). The vault minimum deposit is 1 USDC.
|
|
1326
|
+
3. Waited for yield to accrue; a payment needs the seller's price of spendable yield.
|
|
1327
|
+
|
|
1328
|
+
Then use fetch_with_subly_payment(url) to GET or POST a paid resource from any x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's standard x402 challenge, returning the body plus the payment details. If it returns insufficient_yield, that is expected \u2014 wait for yield, do not loop.`;
|
|
1329
|
+
async function runMcpPaymentServer(config) {
|
|
1330
|
+
const { payer: payer2, signer: signer2, facilitatorBaseUrl: facilitatorBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
|
|
1331
|
+
const server = new Server(
|
|
1332
|
+
{ name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
|
|
1333
|
+
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
1334
|
+
);
|
|
1335
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1336
|
+
tools: [
|
|
1337
|
+
{
|
|
1338
|
+
name: TOOL_NAME,
|
|
1339
|
+
description: `Fetch a URL (GET or POST), automatically paying a standard x402 (HTTP 402) challenge from any x402-compatible seller (Nansen, etc.) out of the agent wallet's Kamino vault yield. Subly realizes just enough yield to the agent's USDC ATA (sponsored) and pays the seller's Solana USDC \`exact\` challenge; the seller needs no Subly integration. Returns the response body and, when a payment was made, the payment details (amount, payee, realize tx). Challenges above maxAmountRawUsdc (default ${defaultMaxAmountRawUsdc2} raw = ${formatRawUsdcAmount(
|
|
1340
|
+
defaultMaxAmountRawUsdc2
|
|
1341
|
+
)} USDC) are refused without paying. Payments are refused when the spendable yield budget cannot cover them \u2014 the principal is never spent. Use only for URLs you intend to purchase access to.`,
|
|
1342
|
+
inputSchema: {
|
|
1343
|
+
type: "object",
|
|
1344
|
+
properties: {
|
|
1345
|
+
url: {
|
|
1346
|
+
type: "string",
|
|
1347
|
+
description: "URL to fetch. Must match the seller's resource URL exactly."
|
|
1348
|
+
},
|
|
1349
|
+
method: {
|
|
1350
|
+
type: "string",
|
|
1351
|
+
description: "HTTP method (default GET). Some x402 sellers deliver the paid resource over POST (e.g. an API that takes a JSON body)."
|
|
1352
|
+
},
|
|
1353
|
+
body: {
|
|
1354
|
+
type: "string",
|
|
1355
|
+
description: "Request body sent on both the probe and the paid retry. Provide a JSON string for POST sellers; sent as content-type application/json unless headers override it."
|
|
1356
|
+
},
|
|
1357
|
+
headers: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
description: "Extra request headers (object of string values), merged into both the probe and the paid retry.",
|
|
1360
|
+
additionalProperties: { type: "string" }
|
|
1361
|
+
},
|
|
1362
|
+
maxAmountRawUsdc: {
|
|
1363
|
+
type: "string",
|
|
1364
|
+
description: 'Refuse (without paying) any challenge above this amount in raw USDC units (6 decimals, e.g. "10000" = 0.01 USDC). Defaults to the server-side cap.'
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
required: ["url"]
|
|
1368
|
+
},
|
|
1369
|
+
annotations: {
|
|
1370
|
+
title: "Fetch with Subly payment",
|
|
1371
|
+
readOnlyHint: false,
|
|
1372
|
+
destructiveHint: true,
|
|
1373
|
+
idempotentHint: false,
|
|
1374
|
+
openWorldHint: true
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
]
|
|
1378
|
+
}));
|
|
1379
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1380
|
+
if (request.params.name !== TOOL_NAME) {
|
|
1381
|
+
return {
|
|
1382
|
+
content: [{ type: "text", text: `unknown tool: ${request.params.name}` }],
|
|
1383
|
+
isError: true
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
const args = request.params.arguments ?? {};
|
|
1387
|
+
const url = args.url;
|
|
1388
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
1389
|
+
return {
|
|
1390
|
+
content: [{ type: "text", text: "missing required argument: url" }],
|
|
1391
|
+
isError: true
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
let maxAmountRawUsdc;
|
|
1395
|
+
if (args.maxAmountRawUsdc !== void 0) {
|
|
1396
|
+
const raw = args.maxAmountRawUsdc;
|
|
1397
|
+
try {
|
|
1398
|
+
if (typeof raw !== "string" && typeof raw !== "number") {
|
|
1399
|
+
throw new TypeError("not a string or number");
|
|
1400
|
+
}
|
|
1401
|
+
maxAmountRawUsdc = BigInt(raw);
|
|
1402
|
+
} catch {
|
|
1403
|
+
return {
|
|
1404
|
+
content: [
|
|
1405
|
+
{
|
|
1406
|
+
type: "text",
|
|
1407
|
+
text: "maxAmountRawUsdc must be an integer raw USDC amount"
|
|
1408
|
+
}
|
|
1409
|
+
],
|
|
1410
|
+
isError: true
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
const method = typeof args.method === "string" ? args.method : void 0;
|
|
1415
|
+
const body = typeof args.body === "string" ? args.body : void 0;
|
|
1416
|
+
const headers = args.headers !== null && typeof args.headers === "object" && !Array.isArray(args.headers) ? Object.fromEntries(
|
|
1417
|
+
Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
|
|
1418
|
+
) : void 0;
|
|
1419
|
+
const mergedHeaders = body === void 0 ? headers : { "content-type": "application/json", ...headers ?? {} };
|
|
1420
|
+
try {
|
|
1421
|
+
const result = await payer2.pay({
|
|
1422
|
+
url,
|
|
1423
|
+
...method === void 0 ? {} : { method },
|
|
1424
|
+
...body === void 0 ? {} : { body },
|
|
1425
|
+
...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
|
|
1426
|
+
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
|
|
1427
|
+
});
|
|
1428
|
+
return {
|
|
1429
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1430
|
+
};
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
if (error instanceof StandardX402PayError) {
|
|
1433
|
+
return {
|
|
1434
|
+
content: [
|
|
1435
|
+
{
|
|
1436
|
+
type: "text",
|
|
1437
|
+
text: JSON.stringify(
|
|
1438
|
+
{
|
|
1439
|
+
paid: false,
|
|
1440
|
+
refused: true,
|
|
1441
|
+
reason: error.reason,
|
|
1442
|
+
message: error.message,
|
|
1443
|
+
detail: error.detail
|
|
1444
|
+
},
|
|
1445
|
+
null,
|
|
1446
|
+
2
|
|
1447
|
+
)
|
|
1448
|
+
}
|
|
1449
|
+
],
|
|
1450
|
+
isError: true
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
content: [
|
|
1455
|
+
{
|
|
1456
|
+
type: "text",
|
|
1457
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1458
|
+
}
|
|
1459
|
+
],
|
|
1460
|
+
isError: true
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
try {
|
|
1465
|
+
await ensureWalletOnboarded({ facilitatorBaseUrl: facilitatorBaseUrl2, signer: signer2 });
|
|
1466
|
+
console.error("[subly-mcp] wallet registered and synced at the relayer");
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
console.error(
|
|
1469
|
+
`[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
|
|
1470
|
+
);
|
|
1494
1471
|
}
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1472
|
+
const transport = new StdioServerTransport();
|
|
1473
|
+
await server.connect(transport);
|
|
1474
|
+
console.error(
|
|
1475
|
+
`[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${facilitatorBaseUrl2}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
|
|
1476
|
+
);
|
|
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;
|
|
1501
1496
|
}
|
|
1502
|
-
|
|
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))
|
|
1512
|
+
);
|
|
1513
|
+
const result = {};
|
|
1514
|
+
for (const table of tables) {
|
|
1515
|
+
if (table.exists) {
|
|
1516
|
+
result[table.address] = table.data.addresses.map(String);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return result;
|
|
1503
1520
|
}
|
|
1504
1521
|
|
|
1505
|
-
// src/client/
|
|
1506
|
-
|
|
1522
|
+
// ../../src/client/relayer-yield-realizer.ts
|
|
1523
|
+
var RelayerRealizeError = class extends Error {
|
|
1524
|
+
constructor(code, message, detail = null) {
|
|
1525
|
+
super(message);
|
|
1526
|
+
this.code = code;
|
|
1527
|
+
this.detail = detail;
|
|
1528
|
+
this.name = "RelayerRealizeError";
|
|
1529
|
+
}
|
|
1530
|
+
code;
|
|
1531
|
+
detail;
|
|
1532
|
+
};
|
|
1533
|
+
var RelayerYieldRealizer = class {
|
|
1534
|
+
config;
|
|
1535
|
+
signer;
|
|
1536
|
+
rpc;
|
|
1537
|
+
fetchImpl;
|
|
1538
|
+
lookupTablesFor;
|
|
1539
|
+
constructor(config) {
|
|
1540
|
+
this.config = {
|
|
1541
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
|
|
1542
|
+
usdcMint: config.usdcMint ?? SUBLY_VAULT.usdcMint,
|
|
1543
|
+
forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
|
|
1544
|
+
};
|
|
1545
|
+
this.signer = config.signer;
|
|
1546
|
+
this.rpc = config.rpc;
|
|
1547
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1548
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
|
|
1549
|
+
}
|
|
1550
|
+
async ensureUsdcAvailable(input) {
|
|
1551
|
+
const agentAta = deriveAssociatedTokenAddress({
|
|
1552
|
+
owner: this.signer.walletAddress,
|
|
1553
|
+
mint: this.config.usdcMint
|
|
1554
|
+
});
|
|
1555
|
+
let shortfallRawUsdc;
|
|
1556
|
+
if (this.config.forceRealizeFullAmount) {
|
|
1557
|
+
shortfallRawUsdc = input.amountRawUsdc;
|
|
1558
|
+
} else {
|
|
1559
|
+
const currentBalance = await this.readTokenBalance(agentAta);
|
|
1560
|
+
if (currentBalance >= input.amountRawUsdc) {
|
|
1561
|
+
return { realizedRawUsdc: 0n, txSignature: null };
|
|
1562
|
+
}
|
|
1563
|
+
shortfallRawUsdc = input.amountRawUsdc - currentBalance;
|
|
1564
|
+
}
|
|
1565
|
+
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1566
|
+
const prepared = await this.postJson("/v1/withdrawals/prepare", {
|
|
1567
|
+
wallet: this.signer.walletAddress,
|
|
1568
|
+
amountRawUsdc: shortfallRawUsdc.toString()
|
|
1569
|
+
});
|
|
1570
|
+
const signed = await this.signer.signWithdrawal({
|
|
1571
|
+
intent: prepared.signingIntent,
|
|
1572
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1573
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1574
|
+
});
|
|
1575
|
+
let settled = await this.postJson("/v1/withdrawals/submit", {
|
|
1576
|
+
withdrawalId: prepared.withdrawalId,
|
|
1577
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1578
|
+
agentSignature: signed.agentSignature
|
|
1579
|
+
});
|
|
1580
|
+
if (settled.status === "submitted") {
|
|
1581
|
+
settled = await this.pollUntilSettled(prepared.withdrawalId);
|
|
1582
|
+
}
|
|
1583
|
+
if (settled.status !== "confirmed" || settled.txSignature === null) {
|
|
1584
|
+
throw new RelayerRealizeError(
|
|
1585
|
+
"realize_not_confirmed",
|
|
1586
|
+
`yield realize withdrawal did not confirm (status=${settled.status})`,
|
|
1587
|
+
settled
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
return {
|
|
1591
|
+
realizedRawUsdc: BigInt(settled.actualWithdrawRawUsdc ?? "0"),
|
|
1592
|
+
txSignature: settled.txSignature
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
/** Refuses to realize more than the ledger's spendable yield (principal). */
|
|
1596
|
+
async assertSpendableYield(shortfallRawUsdc) {
|
|
1597
|
+
const url = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1598
|
+
let response;
|
|
1599
|
+
try {
|
|
1600
|
+
response = await this.fetchImpl(url, {
|
|
1601
|
+
headers: await walletAuthHeaders({
|
|
1602
|
+
signer: this.signer,
|
|
1603
|
+
method: "GET",
|
|
1604
|
+
url
|
|
1605
|
+
})
|
|
1606
|
+
});
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
throw new RelayerRealizeError(
|
|
1609
|
+
"budget_unavailable",
|
|
1610
|
+
"could not read the spendable-yield budget",
|
|
1611
|
+
error
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
if (response.status !== 200) {
|
|
1615
|
+
throw new RelayerRealizeError(
|
|
1616
|
+
"budget_unavailable",
|
|
1617
|
+
`budget endpoint returned ${response.status}`
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
const body = await response.json();
|
|
1621
|
+
const spendable = BigInt(body.budget?.spendableYieldRawUsdc ?? "0");
|
|
1622
|
+
if (spendable < shortfallRawUsdc) {
|
|
1623
|
+
throw new RelayerRealizeError(
|
|
1624
|
+
"insufficient_yield",
|
|
1625
|
+
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
|
|
1626
|
+
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Polls GET /v1/withdrawals/:id (which reconciles a submitted intent against
|
|
1632
|
+
* the chain) until it leaves the "submitted" state or the timeout elapses.
|
|
1633
|
+
*/
|
|
1634
|
+
async pollUntilSettled(withdrawalId, { timeoutMs = 9e4, intervalMs = 2500 } = {}) {
|
|
1635
|
+
const deadline = Date.now() + timeoutMs;
|
|
1636
|
+
let latest = null;
|
|
1637
|
+
while (Date.now() < deadline) {
|
|
1638
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1639
|
+
const url = `${this.config.facilitatorBaseUrl}/v1/withdrawals/${withdrawalId}`;
|
|
1640
|
+
const response = await this.fetchImpl(url, {
|
|
1641
|
+
headers: await walletAuthHeaders({
|
|
1642
|
+
signer: this.signer,
|
|
1643
|
+
method: "GET",
|
|
1644
|
+
url
|
|
1645
|
+
})
|
|
1646
|
+
});
|
|
1647
|
+
if (response.status !== 200) {
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
latest = await response.json();
|
|
1651
|
+
if (latest.status !== "submitted") {
|
|
1652
|
+
return latest;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return latest ?? {
|
|
1656
|
+
status: "submitted",
|
|
1657
|
+
txSignature: null,
|
|
1658
|
+
actualWithdrawRawUsdc: null
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
async postJson(path, body) {
|
|
1662
|
+
const url = `${this.config.facilitatorBaseUrl}${path}`;
|
|
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}`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
return JSON.parse(text);
|
|
1685
|
+
}
|
|
1686
|
+
async readTokenBalance(ata) {
|
|
1687
|
+
try {
|
|
1688
|
+
const response = await this.rpc.getTokenAccountBalance(address2(ata), { commitment: "confirmed" }).send();
|
|
1689
|
+
return BigInt(response.value.amount);
|
|
1690
|
+
} catch {
|
|
1691
|
+
return 0n;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
// ../../src/client/relayer-payer.ts
|
|
1697
|
+
function createRelayerX402Payer(config) {
|
|
1507
1698
|
const realizer = new RelayerYieldRealizer({
|
|
1508
1699
|
facilitatorBaseUrl: config.facilitatorBaseUrl,
|
|
1509
1700
|
signer: config.signer,
|
|
1510
|
-
rpc: config.rpc
|
|
1511
|
-
|
|
1512
|
-
const client = createX402Client({
|
|
1513
|
-
wallet: createX402WalletAdapter(config.agentSecretKey),
|
|
1514
|
-
network: config.network ?? "solana",
|
|
1515
|
-
rpcUrl: config.rpcUrl
|
|
1701
|
+
rpc: config.rpc,
|
|
1702
|
+
forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
|
|
1516
1703
|
});
|
|
1517
|
-
const x402Fetch = (url, init) => client.fetch(url, init);
|
|
1518
1704
|
return new StandardX402Payer({
|
|
1519
1705
|
realizer,
|
|
1520
|
-
x402Fetch,
|
|
1706
|
+
x402Fetch: config.x402Fetch,
|
|
1521
1707
|
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
|
|
1522
1708
|
});
|
|
1523
1709
|
}
|
|
1524
1710
|
|
|
1525
|
-
// src/
|
|
1526
|
-
function formatRawUsdcAmount(raw) {
|
|
1527
|
-
const value = BigInt(raw);
|
|
1528
|
-
const negative = value < 0n;
|
|
1529
|
-
const abs = negative ? -value : value;
|
|
1530
|
-
const whole = abs / 1000000n;
|
|
1531
|
-
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
1532
|
-
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// src/solana/keys.ts
|
|
1711
|
+
// ../../src/solana/keys.ts
|
|
1536
1712
|
import { readFileSync } from "node:fs";
|
|
1537
1713
|
import bs586 from "bs58";
|
|
1538
1714
|
import {
|
|
@@ -1575,18 +1751,35 @@ function loadSecretKeyBytes(params) {
|
|
|
1575
1751
|
throw new Error(`${label} keypair is not configured`);
|
|
1576
1752
|
}
|
|
1577
1753
|
|
|
1578
|
-
// src/solana/rpc.ts
|
|
1754
|
+
// ../../src/solana/rpc.ts
|
|
1579
1755
|
import { createSolanaRpc } from "@solana/kit";
|
|
1580
1756
|
function createRpc(url) {
|
|
1581
1757
|
return createSolanaRpc(url);
|
|
1582
1758
|
}
|
|
1583
1759
|
|
|
1584
|
-
//
|
|
1585
|
-
|
|
1586
|
-
|
|
1760
|
+
// src/svm-x402-fetch.ts
|
|
1761
|
+
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
1762
|
+
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
|
|
1763
|
+
import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
|
|
1764
|
+
async function createSvmX402Fetch(params) {
|
|
1765
|
+
const signer2 = toClientSvmSigner(
|
|
1766
|
+
await createKeyPairSignerFromBytes2(params.agentSecretKey)
|
|
1767
|
+
);
|
|
1768
|
+
const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
|
|
1769
|
+
schemes: [
|
|
1770
|
+
{
|
|
1771
|
+
network: "solana:*",
|
|
1772
|
+
client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
|
|
1773
|
+
}
|
|
1774
|
+
]
|
|
1775
|
+
});
|
|
1776
|
+
return (url, init) => wrapped(url, init);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/mcp-server.ts
|
|
1587
1780
|
var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
1588
1781
|
var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
1589
|
-
var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ?
|
|
1782
|
+
var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
|
|
1590
1783
|
var keyPairSigner = await loadKeyPairSigner({
|
|
1591
1784
|
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1592
1785
|
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
@@ -1599,145 +1792,16 @@ var agentSecretKey = loadSecretKeyBytes({
|
|
|
1599
1792
|
});
|
|
1600
1793
|
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
1601
1794
|
var rpc = createRpc(rpcUrl);
|
|
1602
|
-
var payer =
|
|
1795
|
+
var payer = createRelayerX402Payer({
|
|
1603
1796
|
facilitatorBaseUrl,
|
|
1604
1797
|
signer,
|
|
1605
|
-
agentSecretKey,
|
|
1606
1798
|
rpc,
|
|
1607
|
-
rpcUrl,
|
|
1799
|
+
x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
|
|
1608
1800
|
defaultMaxAmountRawUsdc
|
|
1609
1801
|
});
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
3. Waited for yield to accrue; a payment needs the seller's price of spendable yield.
|
|
1616
|
-
|
|
1617
|
-
Then use fetch_with_subly_payment(url) to GET 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.`;
|
|
1618
|
-
var server = new Server(
|
|
1619
|
-
{ name: "subly-payments", version: "0.2.0" },
|
|
1620
|
-
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
1621
|
-
);
|
|
1622
|
-
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1623
|
-
tools: [
|
|
1624
|
-
{
|
|
1625
|
-
name: TOOL_NAME,
|
|
1626
|
-
description: `GET a URL, 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 ${defaultMaxAmountRawUsdc} raw = ${formatRawUsdcAmount(
|
|
1627
|
-
defaultMaxAmountRawUsdc
|
|
1628
|
-
)} 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.`,
|
|
1629
|
-
inputSchema: {
|
|
1630
|
-
type: "object",
|
|
1631
|
-
properties: {
|
|
1632
|
-
url: {
|
|
1633
|
-
type: "string",
|
|
1634
|
-
description: "URL to fetch (GET). Must match the seller's resource URL exactly."
|
|
1635
|
-
},
|
|
1636
|
-
maxAmountRawUsdc: {
|
|
1637
|
-
type: "string",
|
|
1638
|
-
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.'
|
|
1639
|
-
}
|
|
1640
|
-
},
|
|
1641
|
-
required: ["url"]
|
|
1642
|
-
},
|
|
1643
|
-
annotations: {
|
|
1644
|
-
title: "Fetch with Subly payment",
|
|
1645
|
-
readOnlyHint: false,
|
|
1646
|
-
destructiveHint: true,
|
|
1647
|
-
idempotentHint: false,
|
|
1648
|
-
openWorldHint: true
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
]
|
|
1652
|
-
}));
|
|
1653
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1654
|
-
if (request.params.name !== TOOL_NAME) {
|
|
1655
|
-
return {
|
|
1656
|
-
content: [
|
|
1657
|
-
{ type: "text", text: `unknown tool: ${request.params.name}` }
|
|
1658
|
-
],
|
|
1659
|
-
isError: true
|
|
1660
|
-
};
|
|
1661
|
-
}
|
|
1662
|
-
const args = request.params.arguments ?? {};
|
|
1663
|
-
const url = args.url;
|
|
1664
|
-
if (typeof url !== "string" || url.length === 0) {
|
|
1665
|
-
return {
|
|
1666
|
-
content: [{ type: "text", text: "missing required argument: url" }],
|
|
1667
|
-
isError: true
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
|
-
let maxAmountRawUsdc;
|
|
1671
|
-
if (args.maxAmountRawUsdc !== void 0) {
|
|
1672
|
-
const raw = args.maxAmountRawUsdc;
|
|
1673
|
-
try {
|
|
1674
|
-
if (typeof raw !== "string" && typeof raw !== "number") {
|
|
1675
|
-
throw new TypeError("not a string or number");
|
|
1676
|
-
}
|
|
1677
|
-
maxAmountRawUsdc = BigInt(raw);
|
|
1678
|
-
} catch {
|
|
1679
|
-
return {
|
|
1680
|
-
content: [
|
|
1681
|
-
{
|
|
1682
|
-
type: "text",
|
|
1683
|
-
text: "maxAmountRawUsdc must be an integer raw USDC amount"
|
|
1684
|
-
}
|
|
1685
|
-
],
|
|
1686
|
-
isError: true
|
|
1687
|
-
};
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
try {
|
|
1691
|
-
const result = await payer.pay({
|
|
1692
|
-
url,
|
|
1693
|
-
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
|
|
1694
|
-
});
|
|
1695
|
-
return {
|
|
1696
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1697
|
-
};
|
|
1698
|
-
} catch (error) {
|
|
1699
|
-
if (error instanceof StandardX402PayError) {
|
|
1700
|
-
return {
|
|
1701
|
-
content: [
|
|
1702
|
-
{
|
|
1703
|
-
type: "text",
|
|
1704
|
-
text: JSON.stringify(
|
|
1705
|
-
{
|
|
1706
|
-
paid: false,
|
|
1707
|
-
refused: true,
|
|
1708
|
-
reason: error.reason,
|
|
1709
|
-
message: error.message,
|
|
1710
|
-
detail: error.detail
|
|
1711
|
-
},
|
|
1712
|
-
null,
|
|
1713
|
-
2
|
|
1714
|
-
)
|
|
1715
|
-
}
|
|
1716
|
-
],
|
|
1717
|
-
isError: true
|
|
1718
|
-
};
|
|
1719
|
-
}
|
|
1720
|
-
return {
|
|
1721
|
-
content: [
|
|
1722
|
-
{
|
|
1723
|
-
type: "text",
|
|
1724
|
-
text: error instanceof Error ? error.message : String(error)
|
|
1725
|
-
}
|
|
1726
|
-
],
|
|
1727
|
-
isError: true
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1802
|
+
await runMcpPaymentServer({
|
|
1803
|
+
payer,
|
|
1804
|
+
signer,
|
|
1805
|
+
facilitatorBaseUrl,
|
|
1806
|
+
defaultMaxAmountRawUsdc
|
|
1730
1807
|
});
|
|
1731
|
-
try {
|
|
1732
|
-
await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
|
|
1733
|
-
console.error("[subly-mcp] wallet registered and synced at the relayer");
|
|
1734
|
-
} catch (error) {
|
|
1735
|
-
console.error(
|
|
1736
|
-
`[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
|
|
1737
|
-
);
|
|
1738
|
-
}
|
|
1739
|
-
var transport = new StdioServerTransport();
|
|
1740
|
-
await server.connect(transport);
|
|
1741
|
-
console.error(
|
|
1742
|
-
`[subly-mcp] ready: agent wallet ${signer.walletAddress}, relayer ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc)} USDC`
|
|
1743
|
-
);
|