@agentwonderland/mcp 0.1.22 → 0.1.24

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.
Files changed (74) hide show
  1. package/dist/core/__tests__/card-setup.test.d.ts +1 -0
  2. package/dist/core/__tests__/card-setup.test.js +99 -0
  3. package/dist/core/__tests__/formatters.test.d.ts +1 -0
  4. package/dist/core/__tests__/formatters.test.js +15 -0
  5. package/dist/core/__tests__/passes.test.d.ts +1 -0
  6. package/dist/core/__tests__/passes.test.js +82 -0
  7. package/dist/core/__tests__/payments.test.d.ts +1 -0
  8. package/dist/core/__tests__/payments.test.js +52 -0
  9. package/dist/core/__tests__/principal.test.d.ts +1 -0
  10. package/dist/core/__tests__/principal.test.js +67 -0
  11. package/dist/core/api-client.d.ts +9 -4
  12. package/dist/core/api-client.js +52 -22
  13. package/dist/core/card-setup.d.ts +20 -13
  14. package/dist/core/card-setup.js +87 -30
  15. package/dist/core/config.d.ts +4 -0
  16. package/dist/core/config.js +28 -1
  17. package/dist/core/formatters.d.ts +2 -0
  18. package/dist/core/formatters.js +5 -1
  19. package/dist/core/index.d.ts +2 -0
  20. package/dist/core/index.js +2 -0
  21. package/dist/core/ows-adapter.d.ts +10 -2
  22. package/dist/core/ows-adapter.js +54 -10
  23. package/dist/core/passes.d.ts +40 -0
  24. package/dist/core/passes.js +32 -0
  25. package/dist/core/payments.d.ts +8 -0
  26. package/dist/core/payments.js +121 -16
  27. package/dist/core/principal.d.ts +2 -0
  28. package/dist/core/principal.js +109 -0
  29. package/dist/core/solana-charge.d.ts +9 -0
  30. package/dist/core/solana-charge.js +95 -0
  31. package/dist/core/types.d.ts +10 -0
  32. package/dist/index.js +13 -4
  33. package/dist/prompts/index.js +1 -1
  34. package/dist/resources/wallet.js +8 -1
  35. package/dist/tools/__tests__/_payment-confirmation.test.d.ts +1 -0
  36. package/dist/tools/__tests__/_payment-confirmation.test.js +30 -0
  37. package/dist/tools/_payment-confirmation.d.ts +6 -0
  38. package/dist/tools/_payment-confirmation.js +28 -0
  39. package/dist/tools/agent-info.js +14 -0
  40. package/dist/tools/index.d.ts +1 -0
  41. package/dist/tools/index.js +1 -0
  42. package/dist/tools/passes.d.ts +2 -0
  43. package/dist/tools/passes.js +157 -0
  44. package/dist/tools/run.js +116 -49
  45. package/dist/tools/solve.js +102 -44
  46. package/dist/tools/wallet.js +85 -50
  47. package/package.json +3 -1
  48. package/src/core/__tests__/card-setup.test.ts +118 -0
  49. package/src/core/__tests__/formatters.test.ts +17 -0
  50. package/src/core/__tests__/passes.test.ts +94 -0
  51. package/src/core/__tests__/payments.test.ts +60 -0
  52. package/src/core/__tests__/principal.test.ts +87 -0
  53. package/src/core/api-client.ts +70 -23
  54. package/src/core/card-setup.ts +112 -35
  55. package/src/core/config.ts +33 -2
  56. package/src/core/formatters.ts +7 -1
  57. package/src/core/index.ts +2 -0
  58. package/src/core/ows-adapter.ts +74 -8
  59. package/src/core/passes.ts +74 -0
  60. package/src/core/payments.ts +140 -15
  61. package/src/core/principal.ts +128 -0
  62. package/src/core/solana-charge.ts +149 -0
  63. package/src/core/types.ts +10 -0
  64. package/src/index.ts +13 -4
  65. package/src/prompts/index.ts +1 -1
  66. package/src/resources/wallet.ts +8 -1
  67. package/src/tools/__tests__/_payment-confirmation.test.ts +45 -0
  68. package/src/tools/_payment-confirmation.ts +52 -0
  69. package/src/tools/agent-info.ts +23 -0
  70. package/src/tools/index.ts +1 -0
  71. package/src/tools/passes.ts +234 -0
  72. package/src/tools/run.ts +174 -53
  73. package/src/tools/solve.ts +149 -56
  74. package/src/tools/wallet.ts +102 -52
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import {
14
+ getConfig,
14
15
  getWallets,
15
16
  getDefaultWallet,
16
17
  getCardConfig,
@@ -18,9 +19,23 @@ import {
18
19
  getApiUrl,
19
20
  type WalletEntry,
20
21
  } from "./config.js";
22
+ import type { AgentRecord } from "./types.js";
21
23
 
22
24
  // Cache per wallet+chain combo to avoid re-initializing
23
25
  const fetchCache = new Map<string, typeof fetch>();
26
+ const REGISTRY_METHOD_MAP: Record<string, string> = {
27
+ tempo: "tempo_usdc",
28
+ base: "base_usdc",
29
+ solana: "solana_usdc",
30
+ card: "stripe_card",
31
+ };
32
+
33
+ const METHOD_REGISTRY_MAP: Record<string, string> = {
34
+ tempo_usdc: "tempo",
35
+ base_usdc: "base",
36
+ solana_usdc: "solana",
37
+ stripe_card: "card",
38
+ };
24
39
 
25
40
  // ── Helpers ─────────────────────────────────────────────────────
26
41
 
@@ -32,6 +47,20 @@ function cacheKey(walletId: string, chain: string): string {
32
47
  return `${walletId}:${chain}`;
33
48
  }
34
49
 
50
+ function cardCacheKey(): string | null {
51
+ const card = getCardConfig();
52
+ if (!card) return null;
53
+ return `card:${getApiUrl()}:${card.consumerToken}:${card.paymentMethodId ?? ""}`;
54
+ }
55
+
56
+ function clearStaleCardCache(activeKey?: string): void {
57
+ for (const key of fetchCache.keys()) {
58
+ if (key.startsWith("card:") && key !== activeKey) {
59
+ fetchCache.delete(key);
60
+ }
61
+ }
62
+ }
63
+
35
64
  // ── Per-protocol initializers ───────────────────────────────────
36
65
 
37
66
  async function initMpp(wallet: WalletEntry): Promise<typeof fetch | null> {
@@ -57,6 +86,23 @@ async function initMpp(wallet: WalletEntry): Promise<typeof fetch | null> {
57
86
  }
58
87
  }
59
88
 
89
+ async function initSolanaMpp(wallet: WalletEntry): Promise<typeof fetch | null> {
90
+ try {
91
+ if (wallet.keyType !== "ows" && !wallet.key) {
92
+ return null;
93
+ }
94
+
95
+ const { Mppx } = await import("mppx/client");
96
+ const { solanaChargeClient } = await import("./solana-charge.js");
97
+ const mppx = Mppx.create({
98
+ methods: [solanaChargeClient({ wallet })] as any,
99
+ });
100
+ return mppx.fetch.bind(mppx) as typeof fetch;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
60
106
  async function initCard(): Promise<typeof fetch | null> {
61
107
  const cardConfig = getCardConfig();
62
108
  if (!cardConfig) return null;
@@ -100,7 +146,10 @@ async function initCard(): Promise<typeof fetch | null> {
100
146
  /**
101
147
  * Initialize a payment-aware fetch for a given wallet + chain.
102
148
  */
103
- async function initForChain(wallet: WalletEntry, _chain: string): Promise<typeof fetch | null> {
149
+ async function initForChain(wallet: WalletEntry, chain: string): Promise<typeof fetch | null> {
150
+ if (chain === "solana") {
151
+ return initSolanaMpp(wallet);
152
+ }
104
153
  return initMpp(wallet);
105
154
  }
106
155
 
@@ -115,7 +164,11 @@ async function initForChain(wallet: WalletEntry, _chain: string): Promise<typeof
115
164
  export async function getPaymentFetch(method?: string): Promise<typeof fetch> {
116
165
  // Card payment
117
166
  if (method === "card") {
118
- const ck = "card:card";
167
+ const ck = cardCacheKey();
168
+ clearStaleCardCache(ck ?? undefined);
169
+ if (!ck) {
170
+ throw new Error('Payment method "card" is not configured. Use the wallet_setup tool to configure a wallet');
171
+ }
119
172
  if (fetchCache.has(ck)) return fetchCache.get(ck)!;
120
173
  const pf = await initCard();
121
174
  if (pf) {
@@ -141,23 +194,46 @@ export async function getPaymentFetch(method?: string): Promise<typeof fetch> {
141
194
  throw new Error(`Payment method "${method}" is not configured. Use the wallet_setup tool to configure a wallet`);
142
195
  }
143
196
 
144
- // Auto-detect: try default wallet, then card
197
+ // Auto-detect: try configured methods in order (card first if default)
145
198
  const configured = getConfiguredMethods();
199
+ const defaultMethod = getConfig().defaultPaymentMethod;
200
+
146
201
  for (const m of configured) {
147
202
  if (m === "card") {
148
- const ck = "card:card";
203
+ const ck = cardCacheKey();
204
+ clearStaleCardCache(ck ?? undefined);
205
+ if (!ck) {
206
+ if (m === defaultMethod) {
207
+ const others = configured.filter((x) => x !== m);
208
+ const altText = others.length > 0 ? ` Available alternatives: ${others.join(", ")}` : "";
209
+ throw new Error(`Card payment failed to initialize. Check your card with wallet_status.${altText}`);
210
+ }
211
+ continue;
212
+ }
149
213
  if (fetchCache.has(ck)) return fetchCache.get(ck)!;
150
214
  const pf = await initCard();
151
215
  if (pf) {
152
216
  fetchCache.set(ck, pf);
153
217
  return pf;
154
218
  }
219
+ // If the default method fails, throw instead of silently falling back
220
+ if (m === defaultMethod) {
221
+ const others = configured.filter((x) => x !== m);
222
+ const altText = others.length > 0 ? ` Available alternatives: ${others.join(", ")}` : "";
223
+ throw new Error(`Card payment failed to initialize. Check your card with wallet_status.${altText}`);
224
+ }
155
225
  continue;
156
226
  }
157
227
 
158
228
  // It's a chain name — resolve to wallet
159
229
  const resolved = resolveWalletAndChain(m);
160
- if (!resolved) continue;
230
+ if (!resolved) {
231
+ if (m === defaultMethod) {
232
+ const others = configured.filter((x) => x !== m);
233
+ throw new Error(`Default payment method "${m}" is not configured.${others.length ? ` Alternatives: ${others.join(", ")}` : ""}`);
234
+ }
235
+ continue;
236
+ }
161
237
  const ck = cacheKey(resolved.wallet.id, resolved.chain);
162
238
  if (fetchCache.has(ck)) return fetchCache.get(ck)!;
163
239
  const pf = await initForChain(resolved.wallet, resolved.chain);
@@ -202,9 +278,29 @@ export function getConfiguredMethods(): string[] {
202
278
  methods.push("card");
203
279
  }
204
280
 
281
+ // Respect defaultPaymentMethod — move it to front of list
282
+ const defaultMethod = getConfig().defaultPaymentMethod;
283
+ if (defaultMethod) {
284
+ const idx = methods.indexOf(defaultMethod);
285
+ if (idx > 0) {
286
+ methods.splice(idx, 1);
287
+ methods.unshift(defaultMethod);
288
+ }
289
+ }
290
+
205
291
  return methods;
206
292
  }
207
293
 
294
+ /**
295
+ * Normalize a requested payment method into the MCP-visible method identifier.
296
+ * Accepts chain names, wallet IDs, or "card".
297
+ */
298
+ export function normalizePaymentMethod(method: string): string | null {
299
+ if (method === "card") return "card";
300
+ const resolved = resolveWalletAndChain(method);
301
+ return resolved?.chain ?? null;
302
+ }
303
+
208
304
  /**
209
305
  * Human-friendly display name for a payment method identifier.
210
306
  */
@@ -225,13 +321,33 @@ export function paymentMethodDisplayName(method: string): string {
225
321
  */
226
322
  export function getAcceptedPaymentMethods(): string[] {
227
323
  const methods = getConfiguredMethods();
228
- const map: Record<string, string> = {
229
- tempo: "tempo_usdc",
230
- base: "base_usdc",
231
- solana: "solana_usdc",
232
- card: "stripe_card",
233
- };
234
- return methods.map((m) => map[m]).filter(Boolean);
324
+ return methods
325
+ .map((m) => REGISTRY_METHOD_MAP[m])
326
+ .filter(Boolean);
327
+ }
328
+
329
+ export function toRegistryPaymentMethod(method: string): string | null {
330
+ const normalized = normalizePaymentMethod(method);
331
+ if (!normalized) return null;
332
+ return REGISTRY_METHOD_MAP[normalized] ?? null;
333
+ }
334
+
335
+ export function getCompatiblePaymentMethods(
336
+ agent: Pick<AgentRecord, "payment"> | null | undefined,
337
+ configuredMethods = getConfiguredMethods(),
338
+ ): string[] {
339
+ const acceptedPayments = agent?.payment?.accepted_payments;
340
+ if (!Array.isArray(acceptedPayments) || acceptedPayments.length === 0) {
341
+ return [...configuredMethods];
342
+ }
343
+
344
+ const acceptedMethods = new Set(
345
+ acceptedPayments
346
+ .map((payment) => METHOD_REGISTRY_MAP[payment])
347
+ .filter(Boolean),
348
+ );
349
+
350
+ return configuredMethods.filter((method) => acceptedMethods.has(method));
235
351
  }
236
352
 
237
353
  /**
@@ -245,13 +361,16 @@ export function hasWalletConfigured(): boolean {
245
361
  * Get address for a specific method, or the first configured one.
246
362
  */
247
363
  export async function getWalletAddress(method?: string): Promise<string | null> {
364
+ let chain: string | undefined;
248
365
  let wallet: WalletEntry | undefined;
249
366
 
250
367
  if (method && method !== "card") {
251
368
  const resolved = resolveWalletAndChain(method);
252
369
  wallet = resolved?.wallet;
370
+ chain = resolved?.chain;
253
371
  } else if (!method) {
254
372
  wallet = getDefaultWallet();
373
+ chain = wallet?.defaultChain ?? wallet?.chains[0];
255
374
  }
256
375
 
257
376
  if (!wallet) return null;
@@ -259,14 +378,20 @@ export async function getWalletAddress(method?: string): Promise<string | null>
259
378
  // OWS-managed wallet: derive address via the adapter
260
379
  if (wallet.keyType === "ows" && wallet.owsWalletId) {
261
380
  try {
262
- const { owsAccountFromWalletId } = await import("./ows-adapter.js");
263
- const account = await owsAccountFromWalletId(wallet.owsWalletId);
264
- return account.address;
381
+ const { getOwsWalletAddress } = await import("./ows-adapter.js");
382
+ return await getOwsWalletAddress(
383
+ wallet.owsWalletId,
384
+ chain === "solana" ? "solana" : "evm",
385
+ );
265
386
  } catch {
266
387
  return null;
267
388
  }
268
389
  }
269
390
 
391
+ if (chain === "solana") {
392
+ return null;
393
+ }
394
+
270
395
  // Raw EVM key path
271
396
  if (!wallet.key) return null;
272
397
 
@@ -0,0 +1,128 @@
1
+ import { addWallet, getDefaultWallet, getWallets, type WalletEntry } from "./config.js";
2
+ import { getWalletAddress } from "./payments.js";
3
+ import {
4
+ createOwsWallet,
5
+ isOwsAvailable,
6
+ listOwsWallets,
7
+ } from "./ows-adapter.js";
8
+
9
+ const BASE_CHAIN_ID = "8453";
10
+ const SOLANA_CHAIN_ID = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
11
+ const AUTO_IDENTITY_LABEL = "AW Identity";
12
+
13
+ function principalFromAddress(address: string, type: "evm" | "solana"): string {
14
+ if (type === "evm") {
15
+ return `did:pkh:eip155:${BASE_CHAIN_ID}:${address.toLowerCase()}`;
16
+ }
17
+ return `did:pkh:${SOLANA_CHAIN_ID}:${address}`;
18
+ }
19
+
20
+ function walletSupportsEvm(wallet: WalletEntry): boolean {
21
+ return wallet.chains.some((chain) => chain !== "solana");
22
+ }
23
+
24
+ function walletSupportsSolana(wallet: WalletEntry): boolean {
25
+ return wallet.chains.includes("solana");
26
+ }
27
+
28
+ async function walletPrincipal(wallet: WalletEntry): Promise<string | null> {
29
+ if (walletSupportsEvm(wallet)) {
30
+ const address = await getWalletAddress(wallet.id);
31
+ return address ? principalFromAddress(address, "evm") : null;
32
+ }
33
+
34
+ if (walletSupportsSolana(wallet)) {
35
+ const address = await getWalletAddress(wallet.id);
36
+ return address ? principalFromAddress(address, "solana") : null;
37
+ }
38
+
39
+ return null;
40
+ }
41
+
42
+ function preferredWallets(): WalletEntry[] {
43
+ const wallets = getWallets();
44
+ const defaultWallet = getDefaultWallet();
45
+ const ordered = defaultWallet
46
+ ? [defaultWallet, ...wallets.filter((wallet) => wallet.id !== defaultWallet.id)]
47
+ : wallets;
48
+
49
+ return ordered.sort((a, b) => {
50
+ const aScore = walletSupportsEvm(a) ? 0 : walletSupportsSolana(a) ? 1 : 2;
51
+ const bScore = walletSupportsEvm(b) ? 0 : walletSupportsSolana(b) ? 1 : 2;
52
+ return aScore - bScore;
53
+ });
54
+ }
55
+
56
+ async function createFallbackIdentityWallet(): Promise<WalletEntry> {
57
+ const { generatePrivateKey } = await import("viem/accounts");
58
+ const walletName = `aw-identity-${Date.now()}`;
59
+ const entry: WalletEntry = {
60
+ id: walletName,
61
+ keyType: "evm",
62
+ key: generatePrivateKey(),
63
+ chains: ["tempo", "base"],
64
+ defaultChain: "base",
65
+ label: AUTO_IDENTITY_LABEL,
66
+ };
67
+ addWallet(entry);
68
+ return entry;
69
+ }
70
+
71
+ async function ensureIdentityWallet(): Promise<WalletEntry> {
72
+ const wallets = preferredWallets();
73
+ const existing = wallets.find((wallet) => walletSupportsEvm(wallet) || walletSupportsSolana(wallet));
74
+ if (existing) return existing;
75
+
76
+ if (await isOwsAvailable()) {
77
+ const existingOwsWallets = await listOwsWallets();
78
+ const linked = existingOwsWallets[0];
79
+ if (linked) {
80
+ const entry: WalletEntry = {
81
+ id: linked.name,
82
+ keyType: "ows",
83
+ owsWalletId: linked.id,
84
+ chains: ["tempo", "base"],
85
+ defaultChain: "base",
86
+ label: linked.name,
87
+ };
88
+ addWallet(entry);
89
+ return entry;
90
+ }
91
+
92
+ const walletName = `aw-identity-${Date.now()}`;
93
+ const created = await createOwsWallet(walletName, "evm");
94
+ const entry: WalletEntry = {
95
+ id: walletName,
96
+ keyType: "ows",
97
+ owsWalletId: created.walletId,
98
+ chains: ["tempo", "base"],
99
+ defaultChain: "base",
100
+ label: AUTO_IDENTITY_LABEL,
101
+ };
102
+ addWallet(entry);
103
+ return entry;
104
+ }
105
+
106
+ return createFallbackIdentityWallet();
107
+ }
108
+
109
+ export async function getConsumerPrincipal(): Promise<string | null> {
110
+ for (const wallet of preferredWallets()) {
111
+ const principal = await walletPrincipal(wallet);
112
+ if (principal) return principal;
113
+ }
114
+
115
+ return null;
116
+ }
117
+
118
+ export async function ensureConsumerPrincipal(): Promise<string> {
119
+ const existing = await getConsumerPrincipal();
120
+ if (existing) return existing;
121
+
122
+ const identityWallet = await ensureIdentityWallet();
123
+ const principal = await walletPrincipal(identityWallet);
124
+ if (!principal) {
125
+ throw new Error("Could not derive a consumer principal from the configured identity wallet.");
126
+ }
127
+ return principal;
128
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Custom MPP payment method: Solana USDC charge (client-side).
3
+ *
4
+ * Sends an SPL Token transfer on Solana mainnet and returns the transaction
5
+ * signature as the payment credential.
6
+ */
7
+ import { Credential, Method, z } from "mppx";
8
+ import { Connection, Keypair, PublicKey, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
9
+ import {
10
+ ASSOCIATED_TOKEN_PROGRAM_ID,
11
+ TOKEN_PROGRAM_ID,
12
+ createTransferCheckedInstruction,
13
+ getAssociatedTokenAddressSync,
14
+ } from "@solana/spl-token";
15
+ import type { WalletEntry } from "./config.js";
16
+
17
+ export const SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as const;
18
+ export const SOLANA_CHAIN_ID = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" as const;
19
+ const SOLANA_RPC = "https://api.mainnet-beta.solana.com";
20
+
21
+ const solanaChargeMethod = Method.from({
22
+ name: "solana" as const,
23
+ intent: "charge" as const,
24
+ schema: {
25
+ credential: {
26
+ payload: z.object({
27
+ signature: z.string(),
28
+ type: z.literal("signature"),
29
+ }),
30
+ },
31
+ request: z.pipe(
32
+ z.object({
33
+ amount: z.string(),
34
+ currency: z.string(),
35
+ recipient: z.string(),
36
+ chainId: z.optional(z.string()),
37
+ decimals: z.optional(z.number()),
38
+ }),
39
+ z.transform((value: any) => ({
40
+ ...value,
41
+ methodDetails: { chainId: value.chainId ?? SOLANA_CHAIN_ID },
42
+ })),
43
+ ),
44
+ },
45
+ });
46
+
47
+ function keypairFromPrivateKeyHex(privateKeyHex: string): Keypair {
48
+ const normalized = privateKeyHex.startsWith("0x")
49
+ ? privateKeyHex.slice(2)
50
+ : privateKeyHex;
51
+ const bytes = Uint8Array.from(Buffer.from(normalized, "hex"));
52
+ if (bytes.length === 32) {
53
+ return Keypair.fromSeed(bytes);
54
+ }
55
+ if (bytes.length === 64) {
56
+ return Keypair.fromSecretKey(bytes);
57
+ }
58
+ throw new Error(`Unsupported Solana private key length: ${bytes.length} bytes.`);
59
+ }
60
+
61
+ async function getKeypair(wallet: WalletEntry): Promise<Keypair> {
62
+ if (wallet.keyType === "ows" && wallet.owsWalletId) {
63
+ const { owsSolanaKeypairFromWalletId } = await import("./ows-adapter.js");
64
+ return owsSolanaKeypairFromWalletId(wallet.owsWalletId);
65
+ }
66
+ if (wallet.key) {
67
+ return keypairFromPrivateKeyHex(wallet.key);
68
+ }
69
+ throw new Error(`Wallet "${wallet.id}" cannot sign Solana transactions.`);
70
+ }
71
+
72
+ interface SolanaChargeClientConfig {
73
+ wallet: WalletEntry;
74
+ rpcUrl?: string;
75
+ }
76
+
77
+ async function resolveRecipientTokenAccount(
78
+ connection: Connection,
79
+ mint: PublicKey,
80
+ recipient: PublicKey,
81
+ ): Promise<PublicKey> {
82
+ const accountInfo = await connection.getParsedAccountInfo(recipient, "confirmed");
83
+ const owner = accountInfo.value?.owner;
84
+ if (owner && owner.toBase58() === TOKEN_PROGRAM_ID.toBase58()) {
85
+ return recipient;
86
+ }
87
+
88
+ return getAssociatedTokenAddressSync(
89
+ mint,
90
+ recipient,
91
+ false,
92
+ TOKEN_PROGRAM_ID,
93
+ ASSOCIATED_TOKEN_PROGRAM_ID,
94
+ );
95
+ }
96
+
97
+ export function solanaChargeClient(config: SolanaChargeClientConfig) {
98
+ return Method.toClient(solanaChargeMethod as any, {
99
+ async createCredential({ challenge }: any) {
100
+ const { request } = challenge;
101
+ const amount = BigInt(request.amount);
102
+ const decimals = request.decimals ?? 6;
103
+ const mint = new PublicKey(request.currency ?? SOLANA_USDC_MINT);
104
+ const recipient = new PublicKey(request.recipient);
105
+ const keypair = await getKeypair(config.wallet);
106
+ const owner = keypair.publicKey;
107
+ const connection = new Connection(config.rpcUrl ?? SOLANA_RPC, "confirmed");
108
+
109
+ const sourceTokenAccount = getAssociatedTokenAddressSync(
110
+ mint,
111
+ owner,
112
+ false,
113
+ TOKEN_PROGRAM_ID,
114
+ ASSOCIATED_TOKEN_PROGRAM_ID,
115
+ );
116
+ const destinationTokenAccount = await resolveRecipientTokenAccount(
117
+ connection,
118
+ mint,
119
+ recipient,
120
+ );
121
+
122
+ const instruction = createTransferCheckedInstruction(
123
+ sourceTokenAccount,
124
+ mint,
125
+ destinationTokenAccount,
126
+ owner,
127
+ amount,
128
+ decimals,
129
+ );
130
+
131
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
132
+
133
+ const transaction = new Transaction({
134
+ feePayer: owner,
135
+ recentBlockhash: blockhash,
136
+ }).add(instruction);
137
+
138
+ const signature = await sendAndConfirmTransaction(connection, transaction, [keypair], {
139
+ commitment: "confirmed",
140
+ });
141
+
142
+ return Credential.serialize({
143
+ challenge,
144
+ payload: { signature, type: "signature" as const },
145
+ source: `did:pkh:${SOLANA_CHAIN_ID}:${owner.toBase58()}`,
146
+ });
147
+ },
148
+ });
149
+ }
package/src/core/types.ts CHANGED
@@ -22,6 +22,16 @@ export interface AgentRecord {
22
22
  };
23
23
  payment?: {
24
24
  pricing?: Record<string, unknown>;
25
+ accepted_payments?: string[];
26
+ credit_packs?: {
27
+ unit_type?: string;
28
+ packs?: Array<{
29
+ key?: string;
30
+ name?: string;
31
+ included_units?: number;
32
+ price_usd?: string;
33
+ }>;
34
+ } | null;
25
35
  [key: string]: unknown;
26
36
  };
27
37
  [key: string]: unknown;
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import { registerRateTools } from "./tools/rate.js";
13
13
  import { registerWalletTools } from "./tools/wallet.js";
14
14
  import { registerFavoriteTools } from "./tools/favorites.js";
15
15
  import { registerTipTools } from "./tools/tip.js";
16
+ import { registerPassTools } from "./tools/passes.js";
16
17
 
17
18
  // ── Resources ────────────────────────────────────────────────────
18
19
  import { registerAgentResources } from "./resources/agents.js";
@@ -40,14 +41,21 @@ export async function startMcpServer(): Promise<void> {
40
41
  "1. search_agents() or solve() — find agents for the task",
41
42
  "2. get_agent() — check required input fields before running",
42
43
  "3. run_agent() or solve() with ALL required fields",
43
- "4. If no payment method is set up, run_agent returns a QR code to connect a credit card — present it to the user",
44
+ "3a. If the agent offers discounted credit packs, use buy_agent_credit_pack() and list_agent_credit_packs() when helpful",
45
+ "4. If no payment method is set up, run_agent returns a setup page to connect a credit card — present it to the user",
44
46
  "5. Ask user to rate or tip after a successful run",
45
47
  "",
46
48
  "PAYMENT:",
47
- "- Credit/debit card is the easiest way to pay — just scan a QR code to connect, no funding needed.",
48
- "- Crypto wallets (Tempo USDC, Base USDC) are also supported for advanced users.",
49
+ "- Credit/debit card is the default and easiest way to pay — open the setup page to connect, no funding needed.",
50
+ "- Crypto wallets (Tempo USDC, Base USDC, Solana USDC) are available for advanced users.",
51
+ "- Card and crypto are SEPARATE payment methods. Card charges a credit card; crypto sends USDC on-chain.",
52
+ "- Card is set as the default payment method when configured.",
53
+ "- If multiple valid payment methods are available for a run and pay_with is not specified, ask the user which one to use.",
54
+ "- For fully agentic execution, include pay_with explicitly.",
49
55
  "- Payment is automatic once configured. Users are never charged for failed runs.",
50
- "- Do NOT ask the user to set up payment before they try to run an agent. Let them explore freely — payment setup is handled inline when they're ready to pay.",
56
+ "- Do NOT ask the user to set up payment before they try to run an agent. Let them explore freely.",
57
+ "- If a specific payment method fails, report the error clearly. Do NOT silently fall back to a different method.",
58
+ "- When payment fails, suggest alternatives using wallet_status.",
51
59
  "",
52
60
  "MANAGING PAYMENT METHODS:",
53
61
  "- To remove a card: wallet_setup({ action: \"remove-card\" })",
@@ -70,6 +78,7 @@ export async function startMcpServer(): Promise<void> {
70
78
  registerWalletTools(server);
71
79
  registerFavoriteTools(server);
72
80
  registerTipTools(server);
81
+ registerPassTools(server);
73
82
 
74
83
  // Register resources
75
84
  registerAgentResources(server);
@@ -18,7 +18,7 @@ export function registerPrompts(server: McpServer) {
18
18
  "1. Check my wallet status with wallet_status",
19
19
  "2. If no wallet is configured, help me create one with wallet_setup",
20
20
  "3. Show me some popular agents with search_agents",
21
- "4. Briefly explain how solve, run_agent, rating, and tipping work",
21
+ "4. Briefly explain how solve, run_agent, buy_agent_credit_pack, rating, and tipping work",
22
22
  ].join("\n"),
23
23
  },
24
24
  }],
@@ -1,11 +1,13 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { getWallets, getCardConfig } from "../core/config.js";
2
+ import { getWallets, getCardConfig, getPendingCardSetupToken } from "../core/config.js";
3
+ import { getCardCapabilities } from "../core/card-setup.js";
3
4
  import { getWalletAddress, getConfiguredMethods } from "../core/payments.js";
4
5
 
5
6
  export function registerWalletResources(server: McpServer) {
6
7
  server.resource("wallet-config", "aw://wallet", async () => {
7
8
  const wallets = getWallets();
8
9
  const card = getCardConfig();
10
+ const pendingCardSetupToken = getPendingCardSetupToken();
9
11
  const methods = getConfiguredMethods();
10
12
 
11
13
  const lines = ["Wallet Configuration", ""];
@@ -16,6 +18,11 @@ export function registerWalletResources(server: McpServer) {
16
18
  }
17
19
  if (card) {
18
20
  lines.push(`Card: ${card.brand} ****${card.last4}`);
21
+ const capabilities = await getCardCapabilities();
22
+ lines.push(`Card MPP: ${capabilities.spt_status}${capabilities.message ? ` — ${capabilities.message}` : ""}`);
23
+ }
24
+ if (pendingCardSetupToken) {
25
+ lines.push("Card setup: pending confirmation");
19
26
  }
20
27
  lines.push("", `Configured methods: ${methods.join(", ") || "none"}`);
21
28
 
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ formatPaymentChoicePrompt,
5
+ formatRunConfirmationCommand,
6
+ formatSolveConfirmationCommand,
7
+ makeSolvePendingKey,
8
+ resolveConfirmationMethod,
9
+ } from "../_payment-confirmation.js";
10
+
11
+ describe("payment confirmation helpers", () => {
12
+ it("prefers the explicit method, then pending, then configured default", () => {
13
+ expect(resolveConfirmationMethod("card", "tempo", ["tempo", "card"])).toBe("card");
14
+ expect(resolveConfirmationMethod(undefined, "card", ["tempo", "card"])).toBe("card");
15
+ expect(resolveConfirmationMethod(undefined, undefined, ["card", "tempo"])).toBe("card");
16
+ });
17
+
18
+ it("includes pay_with in run confirmation commands when present", () => {
19
+ expect(formatRunConfirmationCommand("agent_123", "card")).toContain('pay_with: "card"');
20
+ expect(formatRunConfirmationCommand("agent_123", undefined)).not.toContain("pay_with");
21
+ });
22
+
23
+ it("includes pay_with in solve confirmation commands when present", () => {
24
+ expect(formatSolveConfirmationCommand("translate", 1, "card")).toContain('pay_with: "card"');
25
+ expect(formatSolveConfirmationCommand("translate", 1, undefined)).not.toContain("pay_with");
26
+ });
27
+
28
+ it("uses a stable solve pending key for the same request", () => {
29
+ const input = { text: "hello", target_language: "es" };
30
+ expect(makeSolvePendingKey("translate", input, 1)).toBe(
31
+ makeSolvePendingKey("translate", input, 1),
32
+ );
33
+ });
34
+
35
+ it("formats a payment choice prompt with explicit options", () => {
36
+ const prompt = formatPaymentChoicePrompt("Echo Agent", ["card", "tempo"], [
37
+ ' run_agent({ agent_id: "agent_123", input: <same>, pay_with: "card" })',
38
+ ' run_agent({ agent_id: "agent_123", input: <same>, pay_with: "tempo" })',
39
+ ]);
40
+
41
+ expect(prompt).toContain("Multiple payment methods are available for Echo Agent.");
42
+ expect(prompt).toContain('Available methods: "card", "tempo"');
43
+ expect(prompt).toContain('pay_with: "card"');
44
+ });
45
+ });