@arispay/payagent-mcp 2.7.1 → 3.0.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/index.js +452 -446
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
import {
|
|
7
7
|
BootstrapError,
|
|
8
8
|
DelegationClient,
|
|
9
|
-
HostedTopupNotConfiguredError,
|
|
10
9
|
bootstrapAgent,
|
|
11
10
|
formatUSDC,
|
|
12
11
|
getAgent,
|
|
@@ -38,22 +37,8 @@ function platformToolsEnabled(raw = process.env.PAYAGENT_MCP_PROFILE) {
|
|
|
38
37
|
var arispayUrl = getArispayUrl(process.env.ARISPAY_URL);
|
|
39
38
|
var legacyAgentKey = process.env.ARISPAY_AGENT_KEY;
|
|
40
39
|
var legacyWalletAddress = process.env.PAYAGENT_WALLET;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const agents = listAgents();
|
|
44
|
-
return agents[0];
|
|
45
|
-
}
|
|
46
|
-
function resolveAgentKey(name) {
|
|
47
|
-
if (name) {
|
|
48
|
-
const stored = getAgent(name);
|
|
49
|
-
if (stored) return { key: stored.apiKey, source: `agent \`${name}\`` };
|
|
50
|
-
return void 0;
|
|
51
|
-
}
|
|
52
|
-
if (legacyAgentKey) return { key: legacyAgentKey, source: "ARISPAY_AGENT_KEY" };
|
|
53
|
-
const first = listAgents()[0];
|
|
54
|
-
if (first) return { key: first.apiKey, source: `agent \`${first.name}\`` };
|
|
55
|
-
return void 0;
|
|
56
|
-
}
|
|
40
|
+
var SELF_ENDUSER_EXTERNAL_ID = "self";
|
|
41
|
+
var buyformeUrl = process.env.BUYFORME_URL?.replace(/\/$/, "") ?? "https://buyforme.arispay.app";
|
|
57
42
|
function requireDevKey() {
|
|
58
43
|
const key = getApiKey();
|
|
59
44
|
if (!key) {
|
|
@@ -69,34 +54,328 @@ function textResult(text, isError = false) {
|
|
|
69
54
|
...isError ? { isError } : {}
|
|
70
55
|
};
|
|
71
56
|
}
|
|
57
|
+
async function resolveWallet(client, name) {
|
|
58
|
+
const list = await client.listWallets();
|
|
59
|
+
return list.wallets.find((w) => w.name === name);
|
|
60
|
+
}
|
|
61
|
+
async function resolveSelfEndUserId(client) {
|
|
62
|
+
try {
|
|
63
|
+
const user = await client.getEndUserByExternalId(SELF_ENDUSER_EXTERNAL_ID);
|
|
64
|
+
return user.id;
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
72
69
|
var server = new McpServer({
|
|
73
70
|
name: "payagent",
|
|
74
|
-
version: "
|
|
71
|
+
version: "3.0.0"
|
|
75
72
|
});
|
|
73
|
+
server.tool(
|
|
74
|
+
"create_user",
|
|
75
|
+
"Cold-start in one call: create an ArisPay user account plus the master funding account. Safe to re-run \u2014 it recovers the existing account instead of creating duplicates.",
|
|
76
|
+
{
|
|
77
|
+
email: z.string().describe("Email for the new ArisPay account."),
|
|
78
|
+
name: z.string().optional().describe("Human name. Falls back to the email local-part.")
|
|
79
|
+
},
|
|
80
|
+
async ({ email, name }) => {
|
|
81
|
+
try {
|
|
82
|
+
const result = await bootstrapAgent({
|
|
83
|
+
email,
|
|
84
|
+
name,
|
|
85
|
+
arispayUrl: process.env.ARISPAY_URL,
|
|
86
|
+
clientId: "payagent-mcp-bootstrap"
|
|
87
|
+
});
|
|
88
|
+
const devKey = getApiKey();
|
|
89
|
+
if (devKey) {
|
|
90
|
+
const client = new DelegationClient(arispayUrl, devKey);
|
|
91
|
+
try {
|
|
92
|
+
await client.createEndUser({
|
|
93
|
+
externalId: SELF_ENDUSER_EXTERNAL_ID,
|
|
94
|
+
email,
|
|
95
|
+
findOrCreate: true
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const lines = [
|
|
101
|
+
`\u2713 Account: ${result.email} (org \`${result.orgName}\`)`,
|
|
102
|
+
`\u2713 Master funding account ready.`,
|
|
103
|
+
"",
|
|
104
|
+
"Next steps:",
|
|
105
|
+
" 1. create_wallet({ name: 'Daily', perTx, daily, monthly })",
|
|
106
|
+
" 2. fund_wallet({ wallet: 'Daily', rail: 'europ' }) for SEPA / EUR\xD8P",
|
|
107
|
+
" or fund_wallet({ wallet: 'Daily', rail: 'card' }) for card top-up."
|
|
108
|
+
];
|
|
109
|
+
return textResult(lines.join("\n"));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err instanceof BootstrapError) {
|
|
112
|
+
return textResult(`create_user failed (${err.code}): ${err.message}`, true);
|
|
113
|
+
}
|
|
114
|
+
return textResult(
|
|
115
|
+
`create_user failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
116
|
+
true
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
server.tool(
|
|
122
|
+
"create_wallet",
|
|
123
|
+
"Create a sub-wallet / spending profile. Sub-wallets draw from the master funding account and enforce per-transaction, daily, and monthly spend limits.",
|
|
124
|
+
{
|
|
125
|
+
name: z.string().describe("Human name for this sub-wallet, e.g. 'Daily' or 'Travel'."),
|
|
126
|
+
perTx: z.number().int().positive().describe("Per-transaction spend cap in cents."),
|
|
127
|
+
daily: z.number().int().positive().describe("Daily spend cap in cents."),
|
|
128
|
+
monthly: z.number().int().positive().describe("Monthly spend cap in cents."),
|
|
129
|
+
allowedDomains: z.array(z.string()).optional().describe("If provided, restrict this sub-wallet to paying only these domains.")
|
|
130
|
+
},
|
|
131
|
+
async ({ name, perTx, daily, monthly, allowedDomains }) => {
|
|
132
|
+
try {
|
|
133
|
+
const devKey = requireDevKey();
|
|
134
|
+
const client = new DelegationClient(arispayUrl, devKey);
|
|
135
|
+
const wallet = await client.createWallet({
|
|
136
|
+
name,
|
|
137
|
+
perTx,
|
|
138
|
+
daily,
|
|
139
|
+
monthly,
|
|
140
|
+
allowedDomains
|
|
141
|
+
});
|
|
142
|
+
const lines = [
|
|
143
|
+
`\u2713 Sub-wallet \`${wallet.name}\` created.`,
|
|
144
|
+
` id: ${wallet.id}`,
|
|
145
|
+
` limits: ${wallet.limits.perTx ?? "\u2014"}\xA2 / tx, ${wallet.limits.daily ?? "\u2014"}\xA2 / day, ${wallet.limits.monthly ?? "\u2014"}\xA2 / month`,
|
|
146
|
+
"",
|
|
147
|
+
`Fund it with fund_wallet({ wallet: "${wallet.name}", rail: "europ" | "card" }).`
|
|
148
|
+
];
|
|
149
|
+
return textResult(lines.join("\n"));
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return textResult(
|
|
152
|
+
`create_wallet failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
153
|
+
true
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
server.tool(
|
|
159
|
+
"list_wallets",
|
|
160
|
+
"List all sub-wallets under this account.",
|
|
161
|
+
{},
|
|
162
|
+
async () => {
|
|
163
|
+
try {
|
|
164
|
+
const devKey = requireDevKey();
|
|
165
|
+
const client = new DelegationClient(arispayUrl, devKey);
|
|
166
|
+
const list = await client.listWallets();
|
|
167
|
+
if (!list.wallets.length) {
|
|
168
|
+
return textResult(
|
|
169
|
+
"No sub-wallets yet. Create one with create_wallet({ name, perTx, daily, monthly })."
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const lines = list.wallets.map(
|
|
173
|
+
(w) => [
|
|
174
|
+
`\`${w.name}\` (${w.id})`,
|
|
175
|
+
` status: ${w.status}`,
|
|
176
|
+
` limits: ${w.limits.perTx ?? "\u2014"}\xA2 / tx, ${w.limits.daily ?? "\u2014"}\xA2 / day, ${w.limits.monthly ?? "\u2014"}\xA2 / month`,
|
|
177
|
+
` balance: ${w.balance} ${w.balanceCurrency}`
|
|
178
|
+
].join("\n")
|
|
179
|
+
);
|
|
180
|
+
return textResult(lines.join("\n\n"));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
return textResult(
|
|
183
|
+
`list_wallets failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
184
|
+
true
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
server.tool(
|
|
190
|
+
"fund_wallet",
|
|
191
|
+
"Add money to the master funding account so a sub-wallet can spend. rail='europ' opens the Schuman KYC / vIBAN flow in a browser; rail='card' mints a hosted card-setup URL.",
|
|
192
|
+
{
|
|
193
|
+
wallet: z.string().describe("Name of a sub-wallet (used only for handoff context)."),
|
|
194
|
+
rail: z.enum(["europ", "card"]).describe("Funding rail: 'europ' for SEPA/EUR\xD8P, 'card' for debit/credit card."),
|
|
195
|
+
amount: z.number().positive().optional().describe("Optional preset amount in dollars.")
|
|
196
|
+
},
|
|
197
|
+
async ({ wallet, rail, amount }) => {
|
|
198
|
+
try {
|
|
199
|
+
const devKey = requireDevKey();
|
|
200
|
+
const client = new DelegationClient(arispayUrl, devKey);
|
|
201
|
+
const resolved = await resolveWallet(client, wallet);
|
|
202
|
+
if (!resolved) {
|
|
203
|
+
return textResult(`No sub-wallet named \`${wallet}\`. Run list_wallets().`, true);
|
|
204
|
+
}
|
|
205
|
+
if (rail === "europ") {
|
|
206
|
+
const handoffUrl = `${buyformeUrl}/onboarding/fund-europ?agentId=${encodeURIComponent(
|
|
207
|
+
resolved.id
|
|
208
|
+
)}`;
|
|
209
|
+
const lines2 = [
|
|
210
|
+
`Fund the master wallet for \`${wallet}\` with EUR\xD8P via Schuman:`,
|
|
211
|
+
"",
|
|
212
|
+
` ${handoffUrl}`,
|
|
213
|
+
"",
|
|
214
|
+
"Open that link in a browser to:",
|
|
215
|
+
" 1. Verify your identity with Schuman Financial (one-time KYC).",
|
|
216
|
+
" 2. Receive a dedicated SEPA vIBAN.",
|
|
217
|
+
" 3. Send EUR by bank transfer; Schuman mints EUR\xD8P once it clears.",
|
|
218
|
+
"",
|
|
219
|
+
"After the deposit clears, the master funding account is credited and this sub-wallet can spend."
|
|
220
|
+
];
|
|
221
|
+
return textResult(lines2.join("\n"));
|
|
222
|
+
}
|
|
223
|
+
const endUserId = await resolveSelfEndUserId(client);
|
|
224
|
+
if (!endUserId) {
|
|
225
|
+
return textResult(
|
|
226
|
+
"No payer record found. Run create_user first, then retry fund_wallet with rail='card'.",
|
|
227
|
+
true
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const session = await client.createCardSetupSession({ endUserId });
|
|
231
|
+
const lines = [
|
|
232
|
+
`Add a card to fund the master wallet for \`${wallet}\`:`,
|
|
233
|
+
"",
|
|
234
|
+
` ${session.setupUrl}`,
|
|
235
|
+
"",
|
|
236
|
+
` Expires at: ${session.expiresAt}`,
|
|
237
|
+
"",
|
|
238
|
+
"After the card is verified, you can top up. Card top-ups are not yet exposed via MCP; use the BuyForMe web app or the ArisPay dashboard for now."
|
|
239
|
+
];
|
|
240
|
+
return textResult(lines.join("\n"));
|
|
241
|
+
} catch (err) {
|
|
242
|
+
return textResult(
|
|
243
|
+
`fund_wallet failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
244
|
+
true
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
server.tool(
|
|
250
|
+
"get_balance",
|
|
251
|
+
"Show the master funding-account balance(s) and a sub-wallet's spend limits. If no wallet is named, lists all sub-wallets.",
|
|
252
|
+
{
|
|
253
|
+
wallet: z.string().optional().describe("Optional sub-wallet name. Omit to list all sub-wallets.")
|
|
254
|
+
},
|
|
255
|
+
async ({ wallet }) => {
|
|
256
|
+
try {
|
|
257
|
+
const devKey = requireDevKey();
|
|
258
|
+
const client = new DelegationClient(arispayUrl, devKey);
|
|
259
|
+
if (!wallet) {
|
|
260
|
+
const list = await client.listWallets();
|
|
261
|
+
if (!list.wallets.length) {
|
|
262
|
+
return textResult("No sub-wallets yet. Create one with create_wallet().");
|
|
263
|
+
}
|
|
264
|
+
const lines2 = list.wallets.map((w) => {
|
|
265
|
+
const lim = `limits: ${w.limits.perTx ?? "\u2014"}\xA2 / tx, ${w.limits.daily ?? "\u2014"}\xA2 / day, ${w.limits.monthly ?? "\u2014"}\xA2 / month`;
|
|
266
|
+
return `\`${w.name}\` \u2014 ${w.balance} ${w.balanceCurrency} \u2014 ${lim}`;
|
|
267
|
+
});
|
|
268
|
+
return textResult(lines2.join("\n"));
|
|
269
|
+
}
|
|
270
|
+
const resolved = await resolveWallet(client, wallet);
|
|
271
|
+
if (!resolved) {
|
|
272
|
+
return textResult(`No sub-wallet named \`${wallet}\`.`, true);
|
|
273
|
+
}
|
|
274
|
+
const bal = await client.getWalletBalance(resolved.id);
|
|
275
|
+
const lines = [
|
|
276
|
+
`Sub-wallet: \`${bal.wallet.name}\``,
|
|
277
|
+
` status: ${bal.wallet.status}`,
|
|
278
|
+
` limits: ${bal.wallet.limits.perTx ?? "\u2014"}\xA2 / tx, ${bal.wallet.limits.daily ?? "\u2014"}\xA2 / day, ${bal.wallet.limits.monthly ?? "\u2014"}\xA2 / month`,
|
|
279
|
+
"",
|
|
280
|
+
"Master funding accounts:"
|
|
281
|
+
];
|
|
282
|
+
if (!bal.fundingAccounts.length) {
|
|
283
|
+
lines.push(" No funded accounts yet. Use fund_wallet() to add money.");
|
|
284
|
+
} else {
|
|
285
|
+
for (const acct of bal.fundingAccounts) {
|
|
286
|
+
lines.push(` ${acct.currency}: ${acct.balance}\xA2 (${acct.provider ?? "internal"})`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return textResult(lines.join("\n"));
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return textResult(
|
|
292
|
+
`get_balance failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
293
|
+
true
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
);
|
|
298
|
+
server.tool(
|
|
299
|
+
"pay_merchant",
|
|
300
|
+
"Pay a merchant from the master wallet through a sub-wallet. For balance-rail payments (from FundingAccount), merchantId is required. For card-rail payments, the user must have a verified card on file.",
|
|
301
|
+
{
|
|
302
|
+
wallet: z.string().describe("Name of the sub-wallet to spend through."),
|
|
303
|
+
merchantUrl: z.string().describe("Canonical merchant URL for this payment."),
|
|
304
|
+
amount: z.number().int().positive().describe("Amount in cents."),
|
|
305
|
+
memo: z.string().describe("Human-readable memo, e.g. 'T-shirt from getwatta.com'."),
|
|
306
|
+
merchantId: z.string().optional().describe("Required for balance-rail payments to a registered merchant."),
|
|
307
|
+
currency: z.string().optional().describe("Currency code. Default: USD."),
|
|
308
|
+
rail: z.enum(["balance", "card"]).optional().describe("Force balance or card rail. Default: server picks based on available funding.")
|
|
309
|
+
},
|
|
310
|
+
async ({ wallet, merchantUrl, amount, memo, merchantId, currency, rail }) => {
|
|
311
|
+
try {
|
|
312
|
+
const devKey = requireDevKey();
|
|
313
|
+
const client = new DelegationClient(arispayUrl, devKey);
|
|
314
|
+
const resolved = await resolveWallet(client, wallet);
|
|
315
|
+
if (!resolved) {
|
|
316
|
+
return textResult(`No sub-wallet named \`${wallet}\`.`, true);
|
|
317
|
+
}
|
|
318
|
+
const endUserId = await resolveSelfEndUserId(client);
|
|
319
|
+
if (!endUserId) {
|
|
320
|
+
return textResult(
|
|
321
|
+
"No payer record found. Run create_user first, then retry the payment.",
|
|
322
|
+
true
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const payment = await client.createPayment(resolved.id, {
|
|
326
|
+
userId: endUserId,
|
|
327
|
+
amount,
|
|
328
|
+
currency: currency ?? "USD",
|
|
329
|
+
memo,
|
|
330
|
+
merchantUrl,
|
|
331
|
+
merchantId,
|
|
332
|
+
rail
|
|
333
|
+
});
|
|
334
|
+
const lines = [
|
|
335
|
+
`Payment ${payment.id}: ${payment.status}`,
|
|
336
|
+
` rail: ${payment.rail}`,
|
|
337
|
+
` amount: ${payment.amount}\xA2 ${payment.currency}`
|
|
338
|
+
];
|
|
339
|
+
if (payment.merchantName) lines.push(` merchant: ${payment.merchantName}`);
|
|
340
|
+
if (payment.txHash) lines.push(` tx hash: ${payment.txHash}`);
|
|
341
|
+
if (payment.spend) {
|
|
342
|
+
const fmtCap = (v) => v == null ? "no cap" : `${v}\xA2`;
|
|
343
|
+
lines.push(
|
|
344
|
+
` budget: ${fmtCap(payment.spend.remainingDaily)} left today, ${fmtCap(payment.spend.remainingMonthly)} this month`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (payment.nextAction?.challengeUrl) {
|
|
348
|
+
lines.push(` 3DS challenge URL: ${payment.nextAction.challengeUrl}`);
|
|
349
|
+
}
|
|
350
|
+
if (payment.error) {
|
|
351
|
+
lines.push(` error: ${payment.error.code} \u2014 ${payment.error.message}`);
|
|
352
|
+
}
|
|
353
|
+
return textResult(lines.join("\n"), payment.status === "failed");
|
|
354
|
+
} catch (err) {
|
|
355
|
+
return textResult(
|
|
356
|
+
`pay_merchant failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
357
|
+
true
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
);
|
|
76
362
|
server.tool(
|
|
77
363
|
"pay_api",
|
|
78
|
-
"
|
|
364
|
+
"Advanced: make an HTTP request to an x402-protected API, paying with USDC on Base via delegated signing. This is a separate rail from the wallet-centric balance/card flows.",
|
|
79
365
|
{
|
|
80
|
-
url: z.string().describe("The full URL of the API endpoint to call"),
|
|
81
|
-
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).default("GET")
|
|
82
|
-
headers: z.record(z.string(), z.string()).optional()
|
|
83
|
-
body: z.string().optional()
|
|
84
|
-
agent: z.string().optional().describe(
|
|
85
|
-
"Optional: name of a locally-stored agent to pay with. Defaults to ARISPAY_AGENT_KEY or the first stored agent."
|
|
86
|
-
)
|
|
366
|
+
url: z.string().describe("The full URL of the API endpoint to call."),
|
|
367
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).default("GET"),
|
|
368
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
369
|
+
body: z.string().optional(),
|
|
370
|
+
agent: z.string().optional().describe("Optional name of a stored x402 payer agent.")
|
|
87
371
|
},
|
|
88
372
|
async ({ url, method, headers, body, agent }) => {
|
|
89
|
-
const
|
|
90
|
-
if (!
|
|
373
|
+
const stored = agent ? getAgent(agent) : listAgents()[0];
|
|
374
|
+
if (!stored?.apiKey) {
|
|
91
375
|
return textResult(
|
|
92
376
|
[
|
|
93
|
-
"No agent available.
|
|
94
|
-
|
|
95
|
-
"",
|
|
96
|
-
"Or, if a developer key is already paired (`npx payagent init`):",
|
|
97
|
-
" create_agent({ name, perTx, daily, monthly })",
|
|
98
|
-
"",
|
|
99
|
-
"Or set ARISPAY_AGENT_KEY for legacy single-agent mode."
|
|
377
|
+
"No x402 payer agent available. For wallet-centric payments use pay_merchant.",
|
|
378
|
+
"To create an x402 agent, use create_agent({ name, perTx, daily, monthly })."
|
|
100
379
|
].join("\n"),
|
|
101
380
|
true
|
|
102
381
|
);
|
|
@@ -105,28 +384,23 @@ server.tool(
|
|
|
105
384
|
let paymentInfo;
|
|
106
385
|
const fetch402 = payFetchDelegated({
|
|
107
386
|
arispayUrl,
|
|
108
|
-
apiKey:
|
|
387
|
+
apiKey: stored.apiKey,
|
|
109
388
|
onPayment: (info) => {
|
|
110
389
|
paymentInfo = info;
|
|
111
390
|
}
|
|
112
391
|
});
|
|
113
392
|
const response = await fetch402(url, { method, headers, body });
|
|
114
393
|
const responseBody = await response.text();
|
|
115
|
-
const statusLine = paymentInfo ? `HTTP ${response.status} (paid ${paymentInfo.amountCents}\xA2 via
|
|
394
|
+
const statusLine = paymentInfo ? `HTTP ${response.status} (paid ${paymentInfo.amountCents}\xA2 via \`${stored.name}\` \u2014 budget left: ${paymentInfo.remainingDaily}\xA2 today, ${paymentInfo.remainingMonthly}\xA2 this month)` : `HTTP ${response.status} (paid via \`${stored.name}\`)`;
|
|
116
395
|
return textResult([statusLine, "", responseBody].join("\n"));
|
|
117
396
|
} catch (err) {
|
|
118
397
|
const message = err instanceof Error ? err.message : String(err);
|
|
119
398
|
if (looksLikeInsufficientFunds(message)) {
|
|
120
|
-
const onrampHint = await tryGetOnrampHint(agent);
|
|
121
399
|
return textResult(
|
|
122
400
|
[
|
|
123
401
|
`Error: ${message}`,
|
|
124
402
|
"",
|
|
125
|
-
"
|
|
126
|
-
...onrampHint ?? [
|
|
127
|
-
" Use the `fund_agent` tool with hosted=true to mint a Coinbase Onramp URL,",
|
|
128
|
-
" or send USDC on Base directly to the wallet address from `check_wallet`."
|
|
129
|
-
]
|
|
403
|
+
"The x402 agent wallet is out of USDC. Fund it with fund_agent({ name }) or switch to wallet-centric pay_merchant."
|
|
130
404
|
].join("\n"),
|
|
131
405
|
true
|
|
132
406
|
);
|
|
@@ -135,67 +409,17 @@ server.tool(
|
|
|
135
409
|
}
|
|
136
410
|
}
|
|
137
411
|
);
|
|
138
|
-
async function tryGetOnrampHint(agentName) {
|
|
139
|
-
const stored = resolveAgent(agentName);
|
|
140
|
-
if (!stored) return void 0;
|
|
141
|
-
const devKey = getApiKey();
|
|
142
|
-
if (!devKey) return void 0;
|
|
143
|
-
try {
|
|
144
|
-
const client = new DelegationClient(arispayUrl, devKey);
|
|
145
|
-
const link = await client.getHostedTopup(stored.agentId, {});
|
|
146
|
-
return [
|
|
147
|
-
` Coinbase Onramp: ${link.fundingUrl}`,
|
|
148
|
-
` URL valid until: ${link.expiresAt}`,
|
|
149
|
-
` Or send USDC on ${link.network} directly to ${link.walletAddress}`
|
|
150
|
-
];
|
|
151
|
-
} catch (err) {
|
|
152
|
-
if (err instanceof HostedTopupNotConfiguredError) return void 0;
|
|
153
|
-
return void 0;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
server.tool(
|
|
157
|
-
"check_wallet",
|
|
158
|
-
`Show the current agent's credentials, wallet address, and on-chain USDC balance \u2014 the quick "what state am I in" probe.`,
|
|
159
|
-
{
|
|
160
|
-
agent: z.string().optional().describe(
|
|
161
|
-
"Optional: name of a locally-stored agent. Defaults to legacy ARISPAY_WALLET mode."
|
|
162
|
-
)
|
|
163
|
-
},
|
|
164
|
-
async ({ agent }) => {
|
|
165
|
-
const resolved = resolveAgent(agent);
|
|
166
|
-
const walletAddress = resolved?.walletAddress ?? legacyWalletAddress;
|
|
167
|
-
const keyTail = resolved?.apiKey.slice(-4) ?? (legacyAgentKey ? legacyAgentKey.slice(-4) : void 0);
|
|
168
|
-
const lines = [`ArisPay URL: ${arispayUrl}`];
|
|
169
|
-
if (resolved) lines.push(`Agent: ${resolved.name} (${resolved.agentId})`);
|
|
170
|
-
if (keyTail) lines.push(`Agent key: ap_\u2026${keyTail}`);
|
|
171
|
-
if (walletAddress) {
|
|
172
|
-
lines.push(`Wallet: ${walletAddress}`);
|
|
173
|
-
try {
|
|
174
|
-
const raw = await getUSDCBalance(walletAddress, resolved?.network ?? "base");
|
|
175
|
-
lines.push(`Balance: ${formatUSDC(raw)} USDC on ${resolved?.network ?? "base"}`);
|
|
176
|
-
} catch (err) {
|
|
177
|
-
lines.push(
|
|
178
|
-
`Balance: unavailable (${err instanceof Error ? err.message : String(err)})`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
lines.push("", "No wallet known. Use `create_agent` or set PAYAGENT_WALLET for legacy mode.");
|
|
183
|
-
}
|
|
184
|
-
lines.push("", "Manage limits + full history: https://arispay.app/dashboard");
|
|
185
|
-
return textResult(lines.join("\n"));
|
|
186
|
-
}
|
|
187
|
-
);
|
|
188
412
|
server.tool(
|
|
189
413
|
"create_agent",
|
|
190
|
-
"
|
|
414
|
+
"Advanced: provision an x402 payer agent with its own Base USDC wallet. Use create_wallet instead unless you specifically need to pay x402-protected HTTP resources.",
|
|
191
415
|
{
|
|
192
|
-
name: z.string().describe("Local identifier for this agent.
|
|
416
|
+
name: z.string().describe("Local identifier for this x402 agent."),
|
|
193
417
|
perTx: z.number().int().positive().describe("Per-transaction spend cap in cents."),
|
|
194
418
|
daily: z.number().int().positive().describe("Daily spend cap in cents."),
|
|
195
419
|
monthly: z.number().int().positive().describe("Monthly spend cap in cents."),
|
|
196
|
-
allowedDomains: z.array(z.string()).optional()
|
|
197
|
-
network: z.enum(["base", "base-sepolia", "ethereum", "polygon"]).default("base")
|
|
198
|
-
agentType: z.string().optional()
|
|
420
|
+
allowedDomains: z.array(z.string()).optional(),
|
|
421
|
+
network: z.enum(["base", "base-sepolia", "ethereum", "polygon"]).default("base"),
|
|
422
|
+
agentType: z.string().optional()
|
|
199
423
|
},
|
|
200
424
|
async ({ name, perTx, daily, monthly, allowedDomains, network, agentType }) => {
|
|
201
425
|
try {
|
|
@@ -208,13 +432,13 @@ server.tool(
|
|
|
208
432
|
agentType
|
|
209
433
|
});
|
|
210
434
|
const lines = [
|
|
211
|
-
`\u2713
|
|
435
|
+
`\u2713 x402 agent \`${name}\` created.`,
|
|
212
436
|
` Agent ID: ${agent.agentId}`,
|
|
213
437
|
` Wallet: ${agent.walletAddress}`,
|
|
214
438
|
` Network: ${agent.network}`,
|
|
215
439
|
` Limits: ${perTx}\xA2 / tx, ${daily}\xA2 / day, ${monthly}\xA2 / month`,
|
|
216
440
|
"",
|
|
217
|
-
` Fund the wallet with USDC on ${agent.network}, then call
|
|
441
|
+
` Fund the wallet with USDC on ${agent.network}, then call get_balance_agent with name="${name}".`
|
|
218
442
|
];
|
|
219
443
|
return textResult(lines.join("\n"));
|
|
220
444
|
} catch (err) {
|
|
@@ -227,76 +451,33 @@ server.tool(
|
|
|
227
451
|
);
|
|
228
452
|
server.tool(
|
|
229
453
|
"fund_agent",
|
|
230
|
-
"
|
|
454
|
+
"Advanced: get the USDC deposit address for an x402 payer agent.",
|
|
231
455
|
{
|
|
232
|
-
name: z.string().describe("Name of a locally-stored agent.")
|
|
233
|
-
hosted: z.boolean().optional().describe(
|
|
234
|
-
"If true, request a provider-hosted onramp URL (Coinbase, etc.) instead of the plain wallet address. Requires ARISPAY_ONRAMP_PROVIDER on the API deployment."
|
|
235
|
-
),
|
|
236
|
-
amount: z.number().optional().describe("Preset top-up amount in dollars. Only meaningful with `hosted: true`.")
|
|
456
|
+
name: z.string().describe("Name of a locally-stored x402 agent.")
|
|
237
457
|
},
|
|
238
|
-
async ({ name
|
|
458
|
+
async ({ name }) => {
|
|
239
459
|
const stored = getAgent(name);
|
|
240
|
-
if (!stored) return textResult(`No
|
|
241
|
-
if (hosted) {
|
|
242
|
-
const devKey = getApiKey();
|
|
243
|
-
if (!devKey) {
|
|
244
|
-
return textResult("No ArisPay developer key found. Run `npx payagent init` first.", true);
|
|
245
|
-
}
|
|
246
|
-
const client = new DelegationClient(arispayUrl, devKey);
|
|
247
|
-
try {
|
|
248
|
-
const link = await client.getHostedTopup(stored.agentId, { amount });
|
|
249
|
-
const lines2 = [
|
|
250
|
-
`Top up \`${name}\` via ${link.provider}:`,
|
|
251
|
-
"",
|
|
252
|
-
` ${link.fundingUrl}`,
|
|
253
|
-
"",
|
|
254
|
-
` Wallet: ${link.walletAddress}`,
|
|
255
|
-
` Network: ${link.network}`,
|
|
256
|
-
` URL valid until: ${link.expiresAt}`,
|
|
257
|
-
"",
|
|
258
|
-
"Share that URL with the end-user; the onramp deposits USDC straight to the agent's wallet."
|
|
259
|
-
];
|
|
260
|
-
return textResult(lines2.join("\n"));
|
|
261
|
-
} catch (err) {
|
|
262
|
-
if (err instanceof HostedTopupNotConfiguredError) {
|
|
263
|
-
return textResult(
|
|
264
|
-
[
|
|
265
|
-
"Hosted onramp is not configured on this ArisPay deployment.",
|
|
266
|
-
"Falling back to the manual path:",
|
|
267
|
-
"",
|
|
268
|
-
` Send USDC on ${stored.network ?? "base"} to ${stored.walletAddress}`,
|
|
269
|
-
"",
|
|
270
|
-
"Ask the deployment owner to set ARISPAY_ONRAMP_PROVIDER=coinbase (+ COINBASE_ONRAMP_APP_ID) to enable hosted top-ups."
|
|
271
|
-
].join("\n")
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
return textResult(
|
|
275
|
-
`fund_agent (hosted) failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
276
|
-
true
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
460
|
+
if (!stored) return textResult(`No x402 agent named \`${name}\`.`, true);
|
|
280
461
|
const lines = [
|
|
281
|
-
`Fund \`${name}\` by sending USDC on ${stored.network ?? "base"} to:`,
|
|
462
|
+
`Fund x402 agent \`${name}\` by sending USDC on ${stored.network ?? "base"} to:`,
|
|
282
463
|
"",
|
|
283
464
|
` ${stored.walletAddress}`,
|
|
284
465
|
"",
|
|
285
|
-
"
|
|
286
|
-
"Call
|
|
466
|
+
"Only send USDC on the network shown above. Other tokens or networks may be lost.",
|
|
467
|
+
"Call get_balance_agent when the deposit lands."
|
|
287
468
|
];
|
|
288
469
|
return textResult(lines.join("\n"));
|
|
289
470
|
}
|
|
290
471
|
);
|
|
291
472
|
server.tool(
|
|
292
|
-
"
|
|
293
|
-
"
|
|
473
|
+
"get_balance_agent",
|
|
474
|
+
"Advanced: on-chain USDC balance for an x402 payer agent.",
|
|
294
475
|
{
|
|
295
|
-
name: z.string().describe("Name of a locally-stored agent.")
|
|
476
|
+
name: z.string().describe("Name of a locally-stored x402 agent.")
|
|
296
477
|
},
|
|
297
478
|
async ({ name }) => {
|
|
298
479
|
const stored = getAgent(name);
|
|
299
|
-
if (!stored) return textResult(`No
|
|
480
|
+
if (!stored) return textResult(`No x402 agent named \`${name}\`.`, true);
|
|
300
481
|
try {
|
|
301
482
|
const devKey = requireDevKey();
|
|
302
483
|
const client = new DelegationClient(arispayUrl, devKey);
|
|
@@ -310,7 +491,72 @@ server.tool(
|
|
|
310
491
|
return textResult(lines.join("\n"));
|
|
311
492
|
} catch (err) {
|
|
312
493
|
return textResult(
|
|
313
|
-
`
|
|
494
|
+
`get_balance_agent failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
495
|
+
true
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
server.tool(
|
|
501
|
+
"list_agents",
|
|
502
|
+
"Advanced: list x402 payer agents with on-chain wallets.",
|
|
503
|
+
{
|
|
504
|
+
withBalance: z.boolean().optional().describe("Fetch on-chain USDC balance per agent.")
|
|
505
|
+
},
|
|
506
|
+
async ({ withBalance }) => {
|
|
507
|
+
const devKey = getApiKey();
|
|
508
|
+
if (!devKey) {
|
|
509
|
+
return textResult("No developer key found. Run create_user or npx payagent init.");
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const result = await syncAgents({ includeBalance: withBalance === true });
|
|
513
|
+
if (!result.agents.length) {
|
|
514
|
+
return textResult("No x402 agents found. Use create_agent to provision one.");
|
|
515
|
+
}
|
|
516
|
+
const lines = result.agents.map((a) => {
|
|
517
|
+
const balanceLine = a.usdcBalance !== void 0 ? ` balance: ${formatUSDC(BigInt(a.usdcBalance || "0"))} USDC` : null;
|
|
518
|
+
return [
|
|
519
|
+
a.name,
|
|
520
|
+
` agent id: ${a.agentId}`,
|
|
521
|
+
` wallet: ${a.walletAddress}`,
|
|
522
|
+
` network: ${a.network}`,
|
|
523
|
+
` limits: ${a.limits.maxPerTx}\xA2 / tx, ${a.limits.maxDaily}\xA2 / day, ${a.limits.maxMonthly}\xA2 / month`,
|
|
524
|
+
...balanceLine ? [balanceLine] : []
|
|
525
|
+
].join("\n");
|
|
526
|
+
});
|
|
527
|
+
return textResult(lines.join("\n\n"));
|
|
528
|
+
} catch (err) {
|
|
529
|
+
return textResult(
|
|
530
|
+
`list_agents failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
531
|
+
true
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
server.tool(
|
|
537
|
+
"rename_agent",
|
|
538
|
+
"Advanced: rename an x402 payer agent.",
|
|
539
|
+
{
|
|
540
|
+
name: z.string().describe("Current local name of the x402 agent."),
|
|
541
|
+
newName: z.string().describe("New name.")
|
|
542
|
+
},
|
|
543
|
+
async ({ name, newName }) => {
|
|
544
|
+
if (name === newName) {
|
|
545
|
+
return textResult(`Agent \`${name}\` already has that name.`);
|
|
546
|
+
}
|
|
547
|
+
const stored = getAgent(name);
|
|
548
|
+
if (!stored) {
|
|
549
|
+
return textResult(`No x402 agent named \`${name}\`.`, true);
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const devKey = requireDevKey();
|
|
553
|
+
const client = new DelegationClient(arispayUrl, devKey);
|
|
554
|
+
const result = await client.renameAgent(stored.agentId, newName);
|
|
555
|
+
renameStoredAgent(name, result.name);
|
|
556
|
+
return textResult(`\u2713 Renamed \`${name}\` \u2192 \`${result.name}\`.`);
|
|
557
|
+
} catch (err) {
|
|
558
|
+
return textResult(
|
|
559
|
+
`rename_agent failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
314
560
|
true
|
|
315
561
|
);
|
|
316
562
|
}
|
|
@@ -319,24 +565,22 @@ server.tool(
|
|
|
319
565
|
if (platformToolsEnabled()) {
|
|
320
566
|
server.tool(
|
|
321
567
|
"create_enduser",
|
|
322
|
-
"Platform mode: register an end-customer
|
|
568
|
+
"Platform mode: register an end-customer under your account.",
|
|
323
569
|
{
|
|
324
|
-
externalId: z.string().describe("Your own stable id for this customer
|
|
325
|
-
email: z.string().optional()
|
|
326
|
-
findOrCreate: z.boolean().optional()
|
|
570
|
+
externalId: z.string().describe("Your own stable id for this customer."),
|
|
571
|
+
email: z.string().optional(),
|
|
572
|
+
findOrCreate: z.boolean().optional()
|
|
327
573
|
},
|
|
328
574
|
async ({ externalId, email, findOrCreate }) => {
|
|
329
575
|
try {
|
|
330
576
|
const devKey = requireDevKey();
|
|
331
577
|
const client = new DelegationClient(arispayUrl, devKey);
|
|
332
578
|
const user = await client.createEndUser({ externalId, email, findOrCreate });
|
|
333
|
-
|
|
334
|
-
`\u2713 End-user \`${externalId}\` ready
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
];
|
|
339
|
-
return textResult(lines.join("\n"));
|
|
579
|
+
return textResult(
|
|
580
|
+
`\u2713 End-user \`${externalId}\` ready.
|
|
581
|
+
ArisPay id: ${user.id}
|
|
582
|
+
Has card: ${user.hasPaymentMethod ? "yes" : "no"}`
|
|
583
|
+
);
|
|
340
584
|
} catch (err) {
|
|
341
585
|
return textResult(
|
|
342
586
|
`create_enduser failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -347,10 +591,10 @@ if (platformToolsEnabled()) {
|
|
|
347
591
|
);
|
|
348
592
|
server.tool(
|
|
349
593
|
"attach_card_for_user",
|
|
350
|
-
"Platform mode: mint a hosted card-entry URL for an end-user
|
|
594
|
+
"Platform mode: mint a hosted card-entry URL for an end-user.",
|
|
351
595
|
{
|
|
352
|
-
userId: z.string().describe("
|
|
353
|
-
agentName: z.string().optional().describe("Optional
|
|
596
|
+
userId: z.string().describe("ArisPay-internal end-user id."),
|
|
597
|
+
agentName: z.string().optional().describe("Optional x402 agent name to scope the session to.")
|
|
354
598
|
},
|
|
355
599
|
async ({ userId, agentName }) => {
|
|
356
600
|
try {
|
|
@@ -358,17 +602,11 @@ if (platformToolsEnabled()) {
|
|
|
358
602
|
const client = new DelegationClient(arispayUrl, devKey);
|
|
359
603
|
const agentId = agentName ? getAgent(agentName)?.agentId : void 0;
|
|
360
604
|
const session = await client.createCardSetupSession({ endUserId: userId, agentId });
|
|
361
|
-
|
|
362
|
-
`Card-entry URL
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
` Expires at: ${session.expiresAt}`,
|
|
367
|
-
"",
|
|
368
|
-
"Share this URL with the end-user. They enter the card on ArisPay's hosted page; we handle tokenization + 3DS.",
|
|
369
|
-
"Call `get_user_status` afterwards to confirm the card is attached."
|
|
370
|
-
];
|
|
371
|
-
return textResult(lines.join("\n"));
|
|
605
|
+
return textResult(
|
|
606
|
+
[`Card-entry URL:`, ` ${session.setupUrl}`, ` Expires at: ${session.expiresAt}`].join(
|
|
607
|
+
"\n"
|
|
608
|
+
)
|
|
609
|
+
);
|
|
372
610
|
} catch (err) {
|
|
373
611
|
return textResult(
|
|
374
612
|
`attach_card_for_user failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -379,22 +617,20 @@ if (platformToolsEnabled()) {
|
|
|
379
617
|
);
|
|
380
618
|
server.tool(
|
|
381
619
|
"set_user_limits",
|
|
382
|
-
"Platform mode: set per-(user, agent) spend caps
|
|
620
|
+
"Platform mode: set per-(user, agent) spend caps.",
|
|
383
621
|
{
|
|
384
|
-
userId: z.string()
|
|
385
|
-
agentName: z.string()
|
|
386
|
-
perTx: z.number().int().optional()
|
|
387
|
-
daily: z.number().int().optional()
|
|
388
|
-
monthly: z.number().int().optional()
|
|
389
|
-
allowedMcc: z.array(z.string()).optional()
|
|
390
|
-
blockedMcc: z.array(z.string()).optional()
|
|
622
|
+
userId: z.string(),
|
|
623
|
+
agentName: z.string(),
|
|
624
|
+
perTx: z.number().int().optional(),
|
|
625
|
+
daily: z.number().int().optional(),
|
|
626
|
+
monthly: z.number().int().optional(),
|
|
627
|
+
allowedMcc: z.array(z.string()).optional(),
|
|
628
|
+
blockedMcc: z.array(z.string()).optional()
|
|
391
629
|
},
|
|
392
630
|
async ({ userId, agentName, perTx, daily, monthly, allowedMcc, blockedMcc }) => {
|
|
393
631
|
try {
|
|
394
632
|
const stored = getAgent(agentName);
|
|
395
|
-
if (!stored) {
|
|
396
|
-
return textResult(`No locally-stored agent named \`${agentName}\`.`, true);
|
|
397
|
-
}
|
|
633
|
+
if (!stored) return textResult(`No x402 agent named \`${agentName}\`.`, true);
|
|
398
634
|
const devKey = requireDevKey();
|
|
399
635
|
const client = new DelegationClient(arispayUrl, devKey);
|
|
400
636
|
const limit = await client.setUserLimits(userId, {
|
|
@@ -406,10 +642,7 @@ if (platformToolsEnabled()) {
|
|
|
406
642
|
blockedMerchantCategories: blockedMcc
|
|
407
643
|
});
|
|
408
644
|
return textResult(
|
|
409
|
-
|
|
410
|
-
`\u2713 Limits set for \`${userId}\` on agent \`${agentName}\`.`,
|
|
411
|
-
` per-tx ${limit.maxPerTransaction ?? "inherit"}\xA2, daily ${limit.maxDaily ?? "inherit"}\xA2, monthly ${limit.maxMonthly ?? "inherit"}\xA2`
|
|
412
|
-
].join("\n")
|
|
645
|
+
`\u2713 Limits set. per-tx ${limit.maxPerTransaction ?? "inherit"}\xA2, daily ${limit.maxDaily ?? "inherit"}\xA2, monthly ${limit.maxMonthly ?? "inherit"}\xA2`
|
|
413
646
|
);
|
|
414
647
|
} catch (err) {
|
|
415
648
|
return textResult(
|
|
@@ -421,9 +654,9 @@ if (platformToolsEnabled()) {
|
|
|
421
654
|
);
|
|
422
655
|
server.tool(
|
|
423
656
|
"get_user_status",
|
|
424
|
-
"Platform mode: report an end-user's card
|
|
657
|
+
"Platform mode: report an end-user's card and wallet readiness.",
|
|
425
658
|
{
|
|
426
|
-
userId: z.string()
|
|
659
|
+
userId: z.string()
|
|
427
660
|
},
|
|
428
661
|
async ({ userId }) => {
|
|
429
662
|
try {
|
|
@@ -432,21 +665,9 @@ if (platformToolsEnabled()) {
|
|
|
432
665
|
const user = await client.getEndUser(userId);
|
|
433
666
|
const lines = [
|
|
434
667
|
`${user.externalId} (${user.id})`,
|
|
435
|
-
`
|
|
436
|
-
`
|
|
437
|
-
` wallet: ${user.walletAddress ?? "\u2014"}${user.walletChain ? ` on ${user.walletChain}` : ""}`
|
|
668
|
+
` card: ${user.hasPaymentMethod ? `${user.cardBrand ?? "card"} \u2026${user.cardLast4 ?? "????"}` : "\u2014"}`,
|
|
669
|
+
` wallet: ${user.walletAddress ?? "\u2014"}${user.walletChain ? ` on ${user.walletChain}` : ""}`
|
|
438
670
|
];
|
|
439
|
-
if (user.hasWallet) {
|
|
440
|
-
try {
|
|
441
|
-
const w = await client.getWalletStatus(userId);
|
|
442
|
-
lines.push(` balance: ${w.usdcBalance} USDC base units`);
|
|
443
|
-
lines.push(
|
|
444
|
-
` allowance: ${w.usdcAllowance} (${w.allowanceSufficient ? "ok" : "insufficient"})`
|
|
445
|
-
);
|
|
446
|
-
lines.push(` ready: ${w.ready}`);
|
|
447
|
-
} catch {
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
671
|
return textResult(lines.join("\n"));
|
|
451
672
|
} catch (err) {
|
|
452
673
|
return textResult(
|
|
@@ -456,247 +677,32 @@ if (platformToolsEnabled()) {
|
|
|
456
677
|
}
|
|
457
678
|
}
|
|
458
679
|
);
|
|
459
|
-
server.tool(
|
|
460
|
-
"pay_merchant",
|
|
461
|
-
"Direct payment to a known merchant via ArisPay rails (card / crypto / balance / MPP) when you already know the amount. For x402-protected HTTP resources use `pay_api` instead.",
|
|
462
|
-
{
|
|
463
|
-
agentName: z.string().describe("Name of the locally-stored agent making the payment."),
|
|
464
|
-
merchantUrl: z.string().describe("Canonical merchant URL (stored on the payment record)."),
|
|
465
|
-
amount: z.number().int().positive().describe("Amount in cents."),
|
|
466
|
-
memo: z.string().describe("Human-readable memo attached to the payment."),
|
|
467
|
-
userId: z.string().optional().describe("Optional ArisPay end-user id. Required when the agent is in platform mode."),
|
|
468
|
-
rail: z.enum(["card", "crypto", "balance", "mpp"]).optional().describe("Force a specific rail. Default: server-picked based on agent mode + userId."),
|
|
469
|
-
merchantName: z.string().optional().describe("Optional human-readable merchant name."),
|
|
470
|
-
merchantCategoryCode: z.string().optional().describe("MCC for spend-limit enforcement.")
|
|
471
|
-
},
|
|
472
|
-
async ({
|
|
473
|
-
agentName,
|
|
474
|
-
merchantUrl,
|
|
475
|
-
amount,
|
|
476
|
-
memo,
|
|
477
|
-
userId,
|
|
478
|
-
rail,
|
|
479
|
-
merchantName,
|
|
480
|
-
merchantCategoryCode
|
|
481
|
-
}) => {
|
|
482
|
-
try {
|
|
483
|
-
const stored = getAgent(agentName);
|
|
484
|
-
if (!stored) {
|
|
485
|
-
return textResult(`No locally-stored agent named \`${agentName}\`.`, true);
|
|
486
|
-
}
|
|
487
|
-
const devKey = requireDevKey();
|
|
488
|
-
const client = new DelegationClient(arispayUrl, devKey);
|
|
489
|
-
const payment = await client.createPayment(stored.agentId, {
|
|
490
|
-
amount,
|
|
491
|
-
memo,
|
|
492
|
-
merchantUrl,
|
|
493
|
-
merchantName,
|
|
494
|
-
merchantCategoryCode,
|
|
495
|
-
userId,
|
|
496
|
-
rail
|
|
497
|
-
});
|
|
498
|
-
const lines = [
|
|
499
|
-
`Payment ${payment.id}: ${payment.status}`,
|
|
500
|
-
` rail: ${payment.rail}`,
|
|
501
|
-
` amount: ${payment.amount}\xA2 ${payment.currency}`
|
|
502
|
-
];
|
|
503
|
-
if (payment.merchantName) lines.push(` merchant: ${payment.merchantName}`);
|
|
504
|
-
if (payment.txHash) lines.push(` tx hash: ${payment.txHash}`);
|
|
505
|
-
if (payment.spend) {
|
|
506
|
-
const fmtCap = (v) => v == null ? "no cap" : `${v}\xA2`;
|
|
507
|
-
lines.push(
|
|
508
|
-
` budget: ${fmtCap(payment.spend.remainingDaily)} left today, ${fmtCap(payment.spend.remainingMonthly)} this month`
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
if (payment.nextAction) {
|
|
512
|
-
lines.push(` next action: ${payment.nextAction.type}`);
|
|
513
|
-
if (payment.nextAction.challengeUrl) {
|
|
514
|
-
lines.push(` 3DS challenge URL: ${payment.nextAction.challengeUrl}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (payment.error) {
|
|
518
|
-
lines.push(` error: ${payment.error.code} \u2014 ${payment.error.message}`);
|
|
519
|
-
}
|
|
520
|
-
return textResult(lines.join("\n"), payment.status === "failed");
|
|
521
|
-
} catch (err) {
|
|
522
|
-
return textResult(
|
|
523
|
-
`pay_merchant failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
524
|
-
true
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
);
|
|
529
680
|
}
|
|
530
681
|
server.tool(
|
|
531
|
-
"
|
|
532
|
-
"
|
|
533
|
-
{
|
|
534
|
-
email: z.string().describe("Email for the new ArisPay developer account."),
|
|
535
|
-
name: z.string().optional().describe("Human name. Falls back to the email local-part."),
|
|
536
|
-
orgName: z.string().optional().describe(`Workspace / org name. Falls back to "<name>'s Team".`),
|
|
537
|
-
agentName: z.string().optional().describe('Local name for the provisioned agent. Default: "default".'),
|
|
538
|
-
perTx: z.number().int().positive().optional().describe("Per-tx cap in cents. Default: 50."),
|
|
539
|
-
daily: z.number().int().positive().optional().describe("Daily cap in cents. Default: 1000."),
|
|
540
|
-
monthly: z.number().int().positive().optional().describe("Monthly cap in cents. Default: 10000."),
|
|
541
|
-
allowedDomains: z.array(z.string()).optional().describe("Domains the agent may pay. Empty = unrestricted."),
|
|
542
|
-
fund: z.number().positive().optional().describe("Optional preset USD amount; mints a Coinbase Onramp URL pre-set to this value.")
|
|
543
|
-
},
|
|
544
|
-
async ({ email, name, orgName, agentName, perTx, daily, monthly, allowedDomains, fund }) => {
|
|
545
|
-
try {
|
|
546
|
-
const result = await bootstrapAgent({
|
|
547
|
-
email,
|
|
548
|
-
name,
|
|
549
|
-
orgName,
|
|
550
|
-
agentName,
|
|
551
|
-
limits: { perTx, daily, monthly },
|
|
552
|
-
allowedDomains,
|
|
553
|
-
fund,
|
|
554
|
-
arispayUrl: process.env.ARISPAY_URL,
|
|
555
|
-
clientId: "payagent-mcp-bootstrap"
|
|
556
|
-
});
|
|
557
|
-
const lines = [
|
|
558
|
-
`\u2713 Account: ${result.email} (org \`${result.orgName}\`)`,
|
|
559
|
-
`\u2713 Agent: ${result.agentName} (${result.agentId})`,
|
|
560
|
-
` Wallet: ${result.walletAddress} (${result.network})`,
|
|
561
|
-
` Limits: ${result.limits.perTx}\xA2 / tx, ${result.limits.daily}\xA2 / day, ${result.limits.monthly}\xA2 / month`,
|
|
562
|
-
""
|
|
563
|
-
];
|
|
564
|
-
if (result.fundingUrl) {
|
|
565
|
-
lines.push("Fund this wallet via Coinbase Onramp:");
|
|
566
|
-
lines.push(` ${result.fundingUrl}`);
|
|
567
|
-
if (result.fundingExpiresAt) lines.push(` URL valid until: ${result.fundingExpiresAt}`);
|
|
568
|
-
lines.push(` Or send USDC on ${result.network} directly to ${result.walletAddress}.`);
|
|
569
|
-
} else {
|
|
570
|
-
if (result.fundingUrlError) {
|
|
571
|
-
lines.push(`Onramp not available (${result.fundingUrlError}).`);
|
|
572
|
-
}
|
|
573
|
-
lines.push(
|
|
574
|
-
`Fund the wallet by sending USDC on ${result.network} to ${result.walletAddress}.`
|
|
575
|
-
);
|
|
576
|
-
lines.push(
|
|
577
|
-
`Or call \`fund_agent\` with name="${result.agentName}", hosted=true to mint a Coinbase Onramp URL on demand.`
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
if (result.pairing) {
|
|
581
|
-
lines.push("");
|
|
582
|
-
lines.push("\u{1F4F1} Open BuyForMe on your phone (signs you into the same account):");
|
|
583
|
-
lines.push(` ${result.pairing.url}`);
|
|
584
|
-
lines.push(` Link expires: ${result.pairing.expiresAt}`);
|
|
585
|
-
}
|
|
586
|
-
return textResult(lines.join("\n"));
|
|
587
|
-
} catch (err) {
|
|
588
|
-
if (err instanceof BootstrapError) {
|
|
589
|
-
return textResult(`bootstrap_agent failed (${err.code}): ${err.message}`, true);
|
|
590
|
-
}
|
|
591
|
-
return textResult(
|
|
592
|
-
`bootstrap_agent failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
593
|
-
true
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
);
|
|
598
|
-
server.tool(
|
|
599
|
-
"list_agents",
|
|
600
|
-
"List all wallets under this account (server-authoritative; rehydrates the local cache).",
|
|
682
|
+
"check_wallet",
|
|
683
|
+
"Legacy diagnostic for x402 payer agents. Use get_balance for wallet-centric balances.",
|
|
601
684
|
{
|
|
602
|
-
|
|
603
|
-
"When true, fetch on-chain USDC balance per agent (adds one round-trip per wallet)."
|
|
604
|
-
)
|
|
685
|
+
agent: z.string().optional().describe("Optional name of a locally-stored x402 agent.")
|
|
605
686
|
},
|
|
606
|
-
async ({
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
` network: ${a.network ?? "base"}`,
|
|
621
|
-
` limits: ${a.limits.perTx}\xA2 / tx, ${a.limits.daily}\xA2 / day, ${a.limits.monthly}\xA2 / month`,
|
|
622
|
-
` created: ${a.createdAt}`,
|
|
623
|
-
" (local cache only \u2014 set ARISPAY_API_KEY to sync from the server)"
|
|
624
|
-
].join("\n")
|
|
625
|
-
);
|
|
626
|
-
return textResult(lines.join("\n\n"));
|
|
627
|
-
}
|
|
628
|
-
try {
|
|
629
|
-
const result = await syncAgents({ includeBalance: withBalance === true });
|
|
630
|
-
if (!result.agents.length) {
|
|
631
|
-
return textResult(
|
|
632
|
-
"No wallets found under this developer key. Create one with `create_agent` or `bootstrap_agent`."
|
|
633
|
-
);
|
|
687
|
+
async ({ agent }) => {
|
|
688
|
+
const stored = agent ? getAgent(agent) : listAgents()[0];
|
|
689
|
+
const walletAddress = stored?.walletAddress ?? legacyWalletAddress;
|
|
690
|
+
const keyTail = stored?.apiKey.slice(-4) ?? (legacyAgentKey ? legacyAgentKey.slice(-4) : void 0);
|
|
691
|
+
const lines = [`ArisPay URL: ${arispayUrl}`];
|
|
692
|
+
if (stored) lines.push(`Agent: ${stored.name} (${stored.agentId})`);
|
|
693
|
+
if (keyTail) lines.push(`Agent key: ap_\u2026${keyTail}`);
|
|
694
|
+
if (walletAddress) {
|
|
695
|
+
lines.push(`Wallet: ${walletAddress}`);
|
|
696
|
+
try {
|
|
697
|
+
const raw = await getUSDCBalance(walletAddress, stored?.network ?? "base");
|
|
698
|
+
lines.push(`Balance: ${formatUSDC(raw)} USDC on ${stored?.network ?? "base"}`);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
lines.push(`Balance: unavailable (${err instanceof Error ? err.message : String(err)})`);
|
|
634
701
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const fundedLine = a.fundedAt ? ` funded: ${a.fundedAt}` : " funded: not yet";
|
|
638
|
-
const domainsLine = a.allowedDomains.length ? ` domains: ${a.allowedDomains.join(", ")}` : " domains: (unrestricted)";
|
|
639
|
-
const suspendedLine = a.suspended ? " status: SUSPENDED" : null;
|
|
640
|
-
return [
|
|
641
|
-
a.name,
|
|
642
|
-
` agent id: ${a.agentId}`,
|
|
643
|
-
` wallet: ${a.walletAddress}`,
|
|
644
|
-
` network: ${a.network}`,
|
|
645
|
-
` limits: ${a.limits.maxPerTx}\xA2 / tx, ${a.limits.maxDaily}\xA2 / day, ${a.limits.maxMonthly}\xA2 / month`,
|
|
646
|
-
domainsLine,
|
|
647
|
-
fundedLine,
|
|
648
|
-
...balanceLine ? [balanceLine] : [],
|
|
649
|
-
...suspendedLine ? [suspendedLine] : [],
|
|
650
|
-
` created: ${a.createdAt}`
|
|
651
|
-
].join("\n");
|
|
652
|
-
});
|
|
653
|
-
return textResult(
|
|
654
|
-
`${result.agents.length} wallet${result.agents.length === 1 ? "" : "s"} under this account (synced from server):
|
|
655
|
-
|
|
656
|
-
${lines.join("\n\n")}`
|
|
657
|
-
);
|
|
658
|
-
} catch (err) {
|
|
659
|
-
return textResult(
|
|
660
|
-
`list_agents failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
661
|
-
true
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
);
|
|
666
|
-
server.tool(
|
|
667
|
-
"rename_agent",
|
|
668
|
-
"Rename an agent on the server and in the local cache (names are unique per account).",
|
|
669
|
-
{
|
|
670
|
-
name: z.string().describe("Current local name of the agent to rename."),
|
|
671
|
-
newName: z.string().describe(
|
|
672
|
-
"New name. Letters, numbers, spaces, hyphens, underscores, dots. 1-64 chars. Must be unique per account."
|
|
673
|
-
)
|
|
674
|
-
},
|
|
675
|
-
async ({ name, newName }) => {
|
|
676
|
-
if (name === newName) {
|
|
677
|
-
return textResult(`Agent \`${name}\` already has that name \u2014 nothing to do.`);
|
|
678
|
-
}
|
|
679
|
-
const stored = getAgent(name);
|
|
680
|
-
if (!stored) {
|
|
681
|
-
return textResult(
|
|
682
|
-
`No locally-cached agent named \`${name}\`. Run \`list_agents\` first to sync, or check the spelling.`,
|
|
683
|
-
true
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
try {
|
|
687
|
-
const devKey = requireDevKey();
|
|
688
|
-
const client = new DelegationClient(arispayUrl, devKey);
|
|
689
|
-
const result = await client.renameAgent(stored.agentId, newName);
|
|
690
|
-
renameStoredAgent(name, result.name);
|
|
691
|
-
return textResult(
|
|
692
|
-
`\u2713 Renamed \`${name}\` \u2192 \`${result.name}\`. Wallet ${result.walletAddress ?? stored.walletAddress} is unchanged.`
|
|
693
|
-
);
|
|
694
|
-
} catch (err) {
|
|
695
|
-
return textResult(
|
|
696
|
-
`rename_agent failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
697
|
-
true
|
|
698
|
-
);
|
|
702
|
+
} else {
|
|
703
|
+
lines.push("", "No wallet known.");
|
|
699
704
|
}
|
|
705
|
+
return textResult(lines.join("\n"));
|
|
700
706
|
}
|
|
701
707
|
);
|
|
702
708
|
async function main() {
|