@agentwonderland/mcp 0.1.50 → 0.1.52

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 (39) hide show
  1. package/dist/core/__tests__/api-client.test.js +6 -2
  2. package/dist/core/__tests__/payments.test.js +23 -2
  3. package/dist/core/__tests__/principal.test.js +25 -0
  4. package/dist/core/api-client.js +4 -2
  5. package/dist/core/config.d.ts +22 -0
  6. package/dist/core/config.js +20 -0
  7. package/dist/core/link-cli.js +97 -25
  8. package/dist/core/payments.js +20 -6
  9. package/dist/core/principal.d.ts +1 -0
  10. package/dist/core/principal.js +49 -1
  11. package/dist/core/version.d.ts +1 -1
  12. package/dist/core/version.js +1 -1
  13. package/dist/index.js +3 -0
  14. package/dist/tools/__tests__/rebates.test.d.ts +1 -0
  15. package/dist/tools/__tests__/rebates.test.js +72 -0
  16. package/dist/tools/__tests__/wallet.test.js +4 -0
  17. package/dist/tools/index.d.ts +1 -0
  18. package/dist/tools/index.js +1 -0
  19. package/dist/tools/observability.d.ts +2 -0
  20. package/dist/tools/observability.js +20 -0
  21. package/dist/tools/rebates.d.ts +2 -0
  22. package/dist/tools/rebates.js +51 -0
  23. package/dist/tools/wallet.js +11 -5
  24. package/package.json +1 -1
  25. package/src/core/__tests__/api-client.test.ts +8 -1
  26. package/src/core/__tests__/payments.test.ts +36 -2
  27. package/src/core/__tests__/principal.test.ts +31 -0
  28. package/src/core/api-client.ts +4 -2
  29. package/src/core/config.ts +46 -0
  30. package/src/core/link-cli.ts +114 -28
  31. package/src/core/payments.ts +21 -6
  32. package/src/core/principal.ts +54 -1
  33. package/src/core/version.ts +1 -1
  34. package/src/index.ts +3 -0
  35. package/src/tools/__tests__/rebates.test.ts +91 -0
  36. package/src/tools/__tests__/wallet.test.ts +10 -0
  37. package/src/tools/index.ts +1 -0
  38. package/src/tools/rebates.ts +102 -0
  39. package/src/tools/wallet.ts +11 -4
@@ -1,9 +1,11 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
- const { mockGetApiUrl, mockGetApiKey, mockGetPaymentFetch, mockEnsureConsumerPrincipalForMethod, mockGetConsumerPrincipalForMethod, mockGetBaseRebatePrincipal, mockPaymentFetch, } = vi.hoisted(() => ({
2
+ import { MCP_PACKAGE_VERSION } from "../version.js";
3
+ const { mockGetApiUrl, mockGetApiKey, mockGetPaymentFetch, mockEnsureConsumerPrincipalForMethod, mockEnsureBaseRebatePrincipal, mockGetConsumerPrincipalForMethod, mockGetBaseRebatePrincipal, mockPaymentFetch, } = vi.hoisted(() => ({
3
4
  mockGetApiUrl: vi.fn(),
4
5
  mockGetApiKey: vi.fn(),
5
6
  mockGetPaymentFetch: vi.fn(),
6
7
  mockEnsureConsumerPrincipalForMethod: vi.fn(),
8
+ mockEnsureBaseRebatePrincipal: vi.fn(),
7
9
  mockGetConsumerPrincipalForMethod: vi.fn(),
8
10
  mockGetBaseRebatePrincipal: vi.fn(),
9
11
  mockPaymentFetch: vi.fn(),
@@ -17,6 +19,7 @@ vi.mock("../payments.js", () => ({
17
19
  }));
18
20
  vi.mock("../principal.js", () => ({
19
21
  ensureConsumerPrincipal: vi.fn(),
22
+ ensureBaseRebatePrincipal: (...args) => mockEnsureBaseRebatePrincipal(...args),
20
23
  ensureConsumerPrincipalForMethod: (...args) => mockEnsureConsumerPrincipalForMethod(...args),
21
24
  getConsumerPrincipal: vi.fn(),
22
25
  getConsumerPrincipalForMethod: (...args) => mockGetConsumerPrincipalForMethod(...args),
@@ -31,6 +34,7 @@ describe("api-client headers", () => {
31
34
  mockEnsureConsumerPrincipalForMethod.mockResolvedValue("did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF");
32
35
  mockGetConsumerPrincipalForMethod.mockResolvedValue("did:pkh:eip155:8453:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
33
36
  mockGetBaseRebatePrincipal.mockResolvedValue("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
37
+ mockEnsureBaseRebatePrincipal.mockResolvedValue("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
34
38
  mockPaymentFetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), {
35
39
  status: 200,
36
40
  headers: { "content-type": "application/json" },
@@ -46,7 +50,7 @@ describe("api-client headers", () => {
46
50
  "X-AW-Consumer-Principal": "did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
47
51
  "X-AW-Rebate-Principal": "did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
48
52
  "X-AW-Surface": "mcp",
49
- "X-AW-MCP-Version": "0.1.44",
53
+ "X-AW-MCP-Version": MCP_PACKAGE_VERSION,
50
54
  "X-AW-MCP-Tool": "run_agent",
51
55
  "X-AW-MCP-Action": "execute",
52
56
  }),
@@ -21,6 +21,7 @@ vi.mock("../config.js", () => ({
21
21
  getApiUrl: () => "http://api.test",
22
22
  getCardConfig: () => currentCard,
23
23
  getLinkConfig: () => currentLink,
24
+ getLinkCooldown: () => null,
24
25
  getConfig: () => ({ defaultPaymentMethod: currentDefaultPaymentMethod }),
25
26
  getDefaultWallet: () => currentDefaultWallet,
26
27
  getWallets: () => currentWallets,
@@ -124,15 +125,35 @@ describe("payment method initialization", () => {
124
125
  networkId: "profile_test",
125
126
  });
126
127
  expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(expect.objectContaining({
127
- amount: "2000",
128
+ amount: "25",
128
129
  currency: "usd",
129
130
  networkId: "profile_test",
130
131
  paymentMethodId: "csmrpd_link_123",
131
132
  }));
132
133
  const linkTokenRequest = mockCreateLinkSharedPaymentToken.mock.calls[0]?.[0];
133
- expect(linkTokenRequest?.context).toContain("up to USD 20.00");
134
+ expect(linkTokenRequest?.context).toContain("up to USD 0.25");
134
135
  expect(linkTokenRequest?.context).toContain("quoted at USD 0.25");
135
136
  });
137
+ it("uses an explicit Link approval override when configured", async () => {
138
+ process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "2000";
139
+ currentLink = {
140
+ paymentMethodId: "csmrpd_link_123",
141
+ label: "Visa ****4242",
142
+ };
143
+ currentDefaultPaymentMethod = "link";
144
+ const { getPaymentFetch } = await import("../payments.js");
145
+ await getPaymentFetch("link");
146
+ const stripeConfig = mockStripe.mock.calls[0]?.[0];
147
+ await stripeConfig.createToken({
148
+ amount: "25",
149
+ currency: "usd",
150
+ expiresAt: 1778290000,
151
+ networkId: "profile_test",
152
+ });
153
+ expect(mockCreateLinkSharedPaymentToken).toHaveBeenCalledWith(expect.objectContaining({
154
+ amount: "2000",
155
+ }));
156
+ });
136
157
  it("allows a low env override while never approving below the quoted amount", async () => {
137
158
  process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS = "10";
138
159
  currentLink = {
@@ -106,6 +106,19 @@ describe("consumer principal helpers", () => {
106
106
  expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
107
107
  expect(state.addedWallets).toHaveLength(1);
108
108
  });
109
+ it("creates an EVM identity for Link payments when only a Solana wallet exists", async () => {
110
+ state.wallets = [
111
+ { id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
112
+ ];
113
+ state.addresses = {
114
+ "sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
115
+ };
116
+ const { ensureConsumerPrincipalForMethod } = await import("../principal.js");
117
+ const principal = await ensureConsumerPrincipalForMethod("link");
118
+ expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
119
+ expect(state.addedWallets).toHaveLength(1);
120
+ expect(state.addedWallets[0]?.chains).toEqual(["tempo", "base"]);
121
+ });
109
122
  it("returns the Base rebate principal when an EVM wallet is available", async () => {
110
123
  state.wallets = [
111
124
  { id: "aw-main", keyType: "ows", owsWalletId: "ows-main", chains: ["tempo", "base", "solana"], defaultChain: "tempo" },
@@ -119,4 +132,16 @@ describe("consumer principal helpers", () => {
119
132
  const principal = await getBaseRebatePrincipal();
120
133
  expect(principal).toBe("did:pkh:eip155:8453:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
121
134
  });
135
+ it("ensures a Base rebate principal even when the default wallet is Solana-only", async () => {
136
+ state.wallets = [
137
+ { id: "sol-wallet", keyType: "ows", owsWalletId: "ows-sol", chains: ["solana"], defaultChain: "solana" },
138
+ ];
139
+ state.addresses = {
140
+ "sol-wallet": "42W2HfLfveSm1T5et9WTLp2CZ2QXdF2EYCUvyJ2gPpxF",
141
+ };
142
+ const { ensureBaseRebatePrincipal } = await import("../principal.js");
143
+ const principal = await ensureBaseRebatePrincipal();
144
+ expect(principal).toMatch(/^did:pkh:eip155:8453:0x[a-f0-9]{40}$/);
145
+ expect(state.addedWallets).toHaveLength(1);
146
+ });
122
147
  });
@@ -1,6 +1,6 @@
1
1
  import { getApiUrl, getApiKey } from "./config.js";
2
2
  import { getPaymentFetch } from "./payments.js";
3
- import { getBaseRebatePrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipalForMethod, } from "./principal.js";
3
+ import { getBaseRebatePrincipal, ensureBaseRebatePrincipal, ensureConsumerPrincipalForMethod, getConsumerPrincipalForMethod, } from "./principal.js";
4
4
  import { MCP_PACKAGE_VERSION } from "./version.js";
5
5
  // ── Error class ────────────────────────────────────────────────────
6
6
  export class ApiError extends Error {
@@ -49,7 +49,9 @@ async function buildHeaders(path, method, options) {
49
49
  if (principal) {
50
50
  headers["X-AW-Consumer-Principal"] = principal;
51
51
  }
52
- const rebatePrincipal = await getBaseRebatePrincipal();
52
+ const rebatePrincipal = options?.ensureConsumerPrincipal
53
+ ? await ensureBaseRebatePrincipal()
54
+ : await getBaseRebatePrincipal();
53
55
  if (rebatePrincipal) {
54
56
  headers["X-AW-Rebate-Principal"] = rebatePrincipal;
55
57
  }
@@ -22,6 +22,22 @@ export interface PendingLinkSetup {
22
22
  phrase: string;
23
23
  createdAt: string;
24
24
  }
25
+ export interface PendingLinkSpendRequest {
26
+ id: string;
27
+ approvalUrl?: string;
28
+ amount: string;
29
+ currency: string;
30
+ context: string;
31
+ expiresAt: number;
32
+ networkId: string;
33
+ paymentMethodId: string;
34
+ createdAt: string;
35
+ }
36
+ export interface LinkCooldown {
37
+ reason: string;
38
+ createdAt: string;
39
+ blockedUntil: string;
40
+ }
25
41
  export interface SpendPolicy {
26
42
  maxPerTxUsd?: number;
27
43
  maxPerDayUsd?: number;
@@ -43,6 +59,8 @@ export interface Config {
43
59
  card: CardConfig | null;
44
60
  link: LinkConfig | null;
45
61
  pendingLinkSetup?: PendingLinkSetup | null;
62
+ pendingLinkSpendRequest?: PendingLinkSpendRequest | null;
63
+ linkCooldown?: LinkCooldown | null;
46
64
  pendingCardSetupToken?: string | null;
47
65
  favorites: string[];
48
66
  /** Require user confirmation before spending. Default: true. Set false for headless/automated use. */
@@ -103,6 +121,10 @@ export declare function removeWallet(id: string): void;
103
121
  export declare function getCardConfig(): CardConfig | null;
104
122
  export declare function getLinkConfig(): LinkConfig | null;
105
123
  export declare function getPendingLinkSetup(): PendingLinkSetup | null;
124
+ export declare function getPendingLinkSpendRequest(): PendingLinkSpendRequest | null;
125
+ export declare function setPendingLinkSpendRequest(pendingLinkSpendRequest: PendingLinkSpendRequest | null): void;
126
+ export declare function getLinkCooldown(): LinkCooldown | null;
127
+ export declare function setLinkCooldown(linkCooldown: LinkCooldown | null): void;
106
128
  /**
107
129
  * Save card configuration after setup.
108
130
  */
@@ -38,6 +38,8 @@ function migrateIfNeeded(raw) {
38
38
  card: raw.card ?? null,
39
39
  link: raw.link ?? null,
40
40
  pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
41
+ pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
42
+ linkCooldown: raw.linkCooldown ?? null,
41
43
  pendingCardSetupToken: r.pendingCardSetupToken ?? null,
42
44
  favorites: r.favorites ?? [],
43
45
  confirmBeforeSpend: r.confirmBeforeSpend !== false,
@@ -113,6 +115,8 @@ function migrateIfNeeded(raw) {
113
115
  card,
114
116
  link: raw.link ?? null,
115
117
  pendingLinkSetup: raw.link ? null : (raw.pendingLinkSetup ?? null),
118
+ pendingLinkSpendRequest: raw.pendingLinkSpendRequest ?? null,
119
+ linkCooldown: raw.linkCooldown ?? null,
116
120
  pendingCardSetupToken: null,
117
121
  favorites: [],
118
122
  confirmBeforeSpend: true,
@@ -138,6 +142,8 @@ export function getConfig() {
138
142
  card: null,
139
143
  link: null,
140
144
  pendingLinkSetup: null,
145
+ pendingLinkSpendRequest: null,
146
+ linkCooldown: null,
141
147
  pendingCardSetupToken: null,
142
148
  favorites: [],
143
149
  confirmBeforeSpend: true,
@@ -301,6 +307,18 @@ export function getLinkConfig() {
301
307
  export function getPendingLinkSetup() {
302
308
  return getConfig().pendingLinkSetup ?? null;
303
309
  }
310
+ export function getPendingLinkSpendRequest() {
311
+ return getConfig().pendingLinkSpendRequest ?? null;
312
+ }
313
+ export function setPendingLinkSpendRequest(pendingLinkSpendRequest) {
314
+ saveConfig({ pendingLinkSpendRequest });
315
+ }
316
+ export function getLinkCooldown() {
317
+ return getConfig().linkCooldown ?? null;
318
+ }
319
+ export function setLinkCooldown(linkCooldown) {
320
+ saveConfig({ linkCooldown });
321
+ }
304
322
  /**
305
323
  * Save card configuration after setup.
306
324
  */
@@ -330,12 +348,14 @@ export function setLinkConfig(link) {
330
348
  link,
331
349
  defaultPaymentMethod: "link",
332
350
  pendingLinkSetup: null,
351
+ pendingLinkSpendRequest: null,
333
352
  });
334
353
  }
335
354
  else {
336
355
  saveConfig({
337
356
  link,
338
357
  pendingLinkSetup: null,
358
+ pendingLinkSpendRequest: null,
339
359
  defaultPaymentMethod: current.defaultPaymentMethod === "link"
340
360
  ? undefined
341
361
  : current.defaultPaymentMethod,
@@ -1,5 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { getPendingLinkSpendRequest, setLinkCooldown, setPendingLinkSpendRequest, } from "./config.js";
3
4
  const execFileAsync = promisify(execFile);
4
5
  const LINK_CLI_PACKAGE = "@stripe/link-cli";
5
6
  const LINK_CLI_TIMEOUT_MS = 10 * 60 * 1000;
@@ -92,6 +93,34 @@ function extractSpendRequestApproval(output) {
92
93
  }
93
94
  return null;
94
95
  }
96
+ function isMatchingPendingSpendRequest(pending, params) {
97
+ if (!pending)
98
+ return false;
99
+ if (pending.amount !== params.amount)
100
+ return false;
101
+ if (pending.currency.toLowerCase() !== params.currency.toLowerCase())
102
+ return false;
103
+ if (pending.context !== params.context)
104
+ return false;
105
+ if (pending.networkId !== params.networkId)
106
+ return false;
107
+ if (pending.paymentMethodId !== params.paymentMethodId)
108
+ return false;
109
+ if (pending.expiresAt <= Math.floor(Date.now() / 1000))
110
+ return false;
111
+ return true;
112
+ }
113
+ function isProjectedSpendCapError(message) {
114
+ return /projected daily spend|projected spend|exceeds limit/i.test(message);
115
+ }
116
+ function recordLinkCooldown(reason) {
117
+ const now = new Date();
118
+ setLinkCooldown({
119
+ reason,
120
+ createdAt: now.toISOString(),
121
+ blockedUntil: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
122
+ });
123
+ }
95
124
  function normalizePaymentMethods(output) {
96
125
  const values = Array.isArray(output)
97
126
  ? output
@@ -181,6 +210,22 @@ export async function listLinkPaymentMethods() {
181
210
  return normalizePaymentMethods(output);
182
211
  }
183
212
  export async function createLinkSharedPaymentToken(params) {
213
+ const existing = getPendingLinkSpendRequest();
214
+ if (isMatchingPendingSpendRequest(existing, params)) {
215
+ try {
216
+ console.error(`Resuming pending Link approval: ${existing.id}`);
217
+ const spt = await retrieveSharedPaymentToken(existing.id, existing.approvalUrl);
218
+ setPendingLinkSpendRequest(null);
219
+ return spt;
220
+ }
221
+ catch (err) {
222
+ const message = err instanceof Error ? err.message : String(err);
223
+ if (!/denied|expired|not found|without a shared payment token|POLLING_TIMEOUT/i.test(message)) {
224
+ throw err;
225
+ }
226
+ setPendingLinkSpendRequest(null);
227
+ }
228
+ }
184
229
  const args = [
185
230
  "spend-request",
186
231
  "create",
@@ -207,6 +252,16 @@ export async function createLinkSharedPaymentToken(params) {
207
252
  }
208
253
  catch (err) {
209
254
  const message = err instanceof Error ? err.message : String(err);
255
+ if (isProjectedSpendCapError(message)) {
256
+ recordLinkCooldown(message);
257
+ throw new Error([
258
+ "Link is temporarily blocked by Stripe's projected-spend cap.",
259
+ "Reauthing Link or switching cards in the same Link account will not fix this.",
260
+ "Use USDC for now, wait for the rolling Link window to clear, or ask Stripe to raise/clear the merchant projected-spend cap.",
261
+ "",
262
+ message,
263
+ ].join("\n"));
264
+ }
210
265
  if (/invalid network_id|could not retrieve merchant information/i.test(message)) {
211
266
  throw new Error([
212
267
  message,
@@ -219,41 +274,58 @@ export async function createLinkSharedPaymentToken(params) {
219
274
  }
220
275
  const spt = extractSharedPaymentToken(output);
221
276
  if (spt) {
277
+ setPendingLinkSpendRequest(null);
222
278
  return spt;
223
279
  }
224
280
  const approval = extractSpendRequestApproval(output);
225
281
  if (approval?.id && approval.status === "pending_approval") {
282
+ setPendingLinkSpendRequest({
283
+ id: approval.id,
284
+ approvalUrl: approval.approvalUrl,
285
+ amount: params.amount,
286
+ currency: params.currency,
287
+ context: params.context,
288
+ expiresAt: params.expiresAt,
289
+ networkId: params.networkId,
290
+ paymentMethodId: params.paymentMethodId,
291
+ createdAt: new Date().toISOString(),
292
+ });
226
293
  if (approval.approvalUrl) {
227
294
  console.error(`Link approval required: ${approval.approvalUrl}`);
228
295
  }
229
- let retrieved = await runLinkCli([
296
+ const retrievedSpt = await retrieveSharedPaymentToken(approval.id, approval.approvalUrl);
297
+ setPendingLinkSpendRequest(null);
298
+ return retrievedSpt;
299
+ }
300
+ {
301
+ throw new Error("Link spend request completed without a shared payment token in the CLI response.");
302
+ }
303
+ }
304
+ async function retrieveSharedPaymentToken(spendRequestId, approvalUrl) {
305
+ let retrieved = await runLinkCli([
306
+ "spend-request",
307
+ "retrieve",
308
+ spendRequestId,
309
+ "--interval",
310
+ "2",
311
+ "--max-attempts",
312
+ "150",
313
+ ]);
314
+ let retrievedSpt = extractSharedPaymentToken(retrieved);
315
+ for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
316
+ await sleep(2_000);
317
+ retrieved = await runLinkCli([
230
318
  "spend-request",
231
319
  "retrieve",
232
- approval.id,
233
- "--interval",
234
- "2",
235
- "--max-attempts",
236
- "150",
320
+ spendRequestId,
237
321
  ]);
238
- let retrievedSpt = extractSharedPaymentToken(retrieved);
239
- for (let attempt = 0; !retrievedSpt && attempt < 30; attempt += 1) {
240
- await sleep(2_000);
241
- retrieved = await runLinkCli([
242
- "spend-request",
243
- "retrieve",
244
- approval.id,
245
- ]);
246
- retrievedSpt = extractSharedPaymentToken(retrieved);
247
- }
248
- if (retrievedSpt) {
249
- return retrievedSpt;
250
- }
251
- throw new Error([
252
- "Link spend request finished without a shared payment token.",
253
- approval.approvalUrl ? `Approval URL: ${approval.approvalUrl}` : undefined,
254
- ].filter(Boolean).join("\n"));
322
+ retrievedSpt = extractSharedPaymentToken(retrieved);
255
323
  }
256
- {
257
- throw new Error("Link spend request completed without a shared payment token in the CLI response.");
324
+ if (retrievedSpt) {
325
+ return retrievedSpt;
258
326
  }
327
+ throw new Error([
328
+ "Link spend request finished without a shared payment token.",
329
+ approvalUrl ? `Approval URL: ${approvalUrl}` : undefined,
330
+ ].filter(Boolean).join("\n"));
259
331
  }
@@ -9,7 +9,7 @@
9
9
  * Users can configure multiple wallets with different chains and select
10
10
  * which to use per-request via `--pay-with <wallet-id|chain|card>`.
11
11
  */
12
- import { getConfig, getWallets, getDefaultWallet, getCardConfig, getLinkConfig, resolveWalletAndChain, getApiUrl, } from "./config.js";
12
+ import { getConfig, getWallets, getDefaultWallet, getCardConfig, getLinkConfig, getLinkCooldown, resolveWalletAndChain, getApiUrl, } from "./config.js";
13
13
  // Feature flag: disable card payment for launch. Flip to true once Stripe
14
14
  // approves the live SPT issuer. Config/card state is still persisted so this
15
15
  // can be re-enabled without reconfiguring wallets.
@@ -26,7 +26,6 @@ const REGISTRY_METHOD_MAP = {
26
26
  card: "stripe_card",
27
27
  link: "stripe_card",
28
28
  };
29
- const DEFAULT_LINK_APPROVAL_LIMIT_CENTS = 2_000;
30
29
  const ACCEPTED_PAYMENT_ALIASES = {
31
30
  tempo: ["tempo_usdc", "tempo"],
32
31
  base: ["base_usdc", "base"],
@@ -164,10 +163,24 @@ function formatMinorCurrencyAmount(currency, amount) {
164
163
  function getLinkApprovalLimitAmount(actualAmount) {
165
164
  const actualAmountCents = Number(actualAmount);
166
165
  const configuredLimit = Number(process.env.AGENTWONDERLAND_LINK_APPROVAL_LIMIT_CENTS);
167
- const defaultLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
168
- ? Math.floor(configuredLimit)
169
- : DEFAULT_LINK_APPROVAL_LIMIT_CENTS;
170
- return String(Math.max(actualAmountCents, defaultLimit));
166
+ const approvalLimit = Number.isFinite(configuredLimit) && configuredLimit > 0
167
+ ? Math.max(actualAmountCents, Math.floor(configuredLimit))
168
+ : actualAmountCents;
169
+ return String(approvalLimit);
170
+ }
171
+ function assertLinkNotCoolingDown() {
172
+ const cooldown = getLinkCooldown();
173
+ if (!cooldown)
174
+ return;
175
+ const blockedUntil = Date.parse(cooldown.blockedUntil);
176
+ if (!Number.isFinite(blockedUntil) || blockedUntil <= Date.now())
177
+ return;
178
+ throw new Error([
179
+ "Link is temporarily blocked by Stripe's projected-spend cap.",
180
+ "Reauthing Link or switching cards in the same Link account will not fix this.",
181
+ `Try again after ${cooldown.blockedUntil}, use USDC, or ask Stripe to raise/clear the merchant projected-spend cap.`,
182
+ `Last Link error: ${cooldown.reason}`,
183
+ ].join("\n"));
171
184
  }
172
185
  function buildLinkApprovalContext(params) {
173
186
  const amountText = formatMinorCurrencyAmount(params.currency, params.amount);
@@ -188,6 +201,7 @@ async function initLink() {
188
201
  methods: [stripe({
189
202
  paymentMethod: linkConfig.paymentMethodId,
190
203
  createToken: async (params) => {
204
+ assertLinkNotCoolingDown();
191
205
  const approvalAmount = getLinkApprovalLimitAmount(params.amount);
192
206
  console.error(`Requesting Link approval up to ${formatMinorCurrencyAmount(params.currency, approvalAmount)} ` +
193
207
  `for this ${formatMinorCurrencyAmount(params.currency, params.amount)} Agent Wonderland run.`);
@@ -1,5 +1,6 @@
1
1
  export declare function getConsumerPrincipal(): Promise<string | null>;
2
2
  export declare function getBaseRebatePrincipal(): Promise<string | null>;
3
+ export declare function ensureBaseRebatePrincipal(): Promise<string>;
3
4
  export declare function getConsumerPrincipalForMethod(method?: string): Promise<string | null>;
4
5
  export declare function ensureConsumerPrincipal(): Promise<string>;
5
6
  export declare function ensureConsumerPrincipalForMethod(method?: string): Promise<string>;
@@ -59,6 +59,40 @@ async function createFallbackIdentityWallet() {
59
59
  addWallet(entry);
60
60
  return entry;
61
61
  }
62
+ async function ensureEvmIdentityWallet() {
63
+ const existing = preferredWallets().find((wallet) => walletSupportsEvm(wallet));
64
+ if (existing)
65
+ return existing;
66
+ if (await isOwsAvailable()) {
67
+ const existingOwsWallets = await listOwsWallets();
68
+ const linked = existingOwsWallets[0];
69
+ if (linked) {
70
+ const entry = {
71
+ id: linked.name,
72
+ keyType: "ows",
73
+ owsWalletId: linked.id,
74
+ chains: ["tempo", "base"],
75
+ defaultChain: "base",
76
+ label: linked.name,
77
+ };
78
+ addWallet(entry);
79
+ return entry;
80
+ }
81
+ const walletName = `aw-identity-${Date.now()}`;
82
+ const created = await createOwsWallet(walletName, "evm");
83
+ const entry = {
84
+ id: walletName,
85
+ keyType: "ows",
86
+ owsWalletId: created.walletId,
87
+ chains: ["tempo", "base"],
88
+ defaultChain: "base",
89
+ label: AUTO_IDENTITY_LABEL,
90
+ };
91
+ addWallet(entry);
92
+ return entry;
93
+ }
94
+ return createFallbackIdentityWallet();
95
+ }
62
96
  async function ensureIdentityWallet() {
63
97
  const wallets = preferredWallets();
64
98
  const existing = wallets.find((wallet) => walletSupportsEvm(wallet) || walletSupportsSolana(wallet));
@@ -105,9 +139,20 @@ export async function getConsumerPrincipal() {
105
139
  export async function getBaseRebatePrincipal() {
106
140
  return (await principalForChain("base")) ?? principalForChain("tempo");
107
141
  }
142
+ export async function ensureBaseRebatePrincipal() {
143
+ const existing = await getBaseRebatePrincipal();
144
+ if (existing)
145
+ return existing;
146
+ const wallet = await ensureEvmIdentityWallet();
147
+ const principal = await walletPrincipal(wallet);
148
+ if (!principal || !principal.startsWith(`did:pkh:eip155:${BASE_CHAIN_ID}:`)) {
149
+ throw new Error("Could not derive a Base rebate principal from the configured identity wallet.");
150
+ }
151
+ return principal;
152
+ }
108
153
  export async function getConsumerPrincipalForMethod(method) {
109
154
  if (!method || method === "card" || method === "link") {
110
- return getConsumerPrincipal();
155
+ return getBaseRebatePrincipal();
111
156
  }
112
157
  const resolved = resolveWalletAndChain(method);
113
158
  if (!resolved)
@@ -129,6 +174,9 @@ export async function ensureConsumerPrincipalForMethod(method) {
129
174
  const existing = await getConsumerPrincipalForMethod(method);
130
175
  if (existing)
131
176
  return existing;
177
+ if (!method || method === "card" || method === "link") {
178
+ return ensureBaseRebatePrincipal();
179
+ }
132
180
  if (method && method !== "card" && method !== "link") {
133
181
  throw new Error(`Could not derive a consumer principal for payment method "${method}". ` +
134
182
  "Check wallet_status and confirm that chain is configured for the active wallet.");
@@ -1 +1 @@
1
- export declare const MCP_PACKAGE_VERSION = "0.1.50";
1
+ export declare const MCP_PACKAGE_VERSION = "0.1.51";
@@ -1 +1 @@
1
- export const MCP_PACKAGE_VERSION = "0.1.50";
1
+ export const MCP_PACKAGE_VERSION = "0.1.51";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { registerWalletTools } from "./tools/wallet.js";
12
12
  import { registerFavoriteTools } from "./tools/favorites.js";
13
13
  import { registerTipTools } from "./tools/tip.js";
14
14
  import { registerPassTools } from "./tools/passes.js";
15
+ import { registerRebateTools } from "./tools/rebates.js";
15
16
  import { registerUploadTools } from "./tools/upload.js";
16
17
  import { registerProbeTools } from "./tools/probe.js";
17
18
  import { registerProviderTools } from "./tools/providers.js";
@@ -64,6 +65,7 @@ export async function startMcpServer() {
64
65
  "",
65
66
  "WALLET HYGIENE:",
66
67
  "- wallet_status shows per-chain USDC balance and the active network (mainnet vs testnet).",
68
+ "- rebate_status shows accrued, pending, paid, and blocked consumer rebates plus recent payout transactions.",
67
69
  "- To set up payments: wallet_setup({ action: \"start\" }). Link card/bank is recommended for most users.",
68
70
  "- To create or import a crypto wallet directly: wallet_setup({ action: \"create\" }) or { action: \"import\", key }.",
69
71
  "- NEVER delete or rotate keys programmatically. Direct users to edit ~/.agentwonderland/config.json or ~/.ows/ manually.",
@@ -80,6 +82,7 @@ export async function startMcpServer() {
80
82
  registerFavoriteTools(server);
81
83
  registerTipTools(server);
82
84
  registerPassTools(server);
85
+ registerRebateTools(server);
83
86
  registerUploadTools(server);
84
87
  registerProbeTools(server);
85
88
  registerProviderTools(server);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const { mockApiGet } = vi.hoisted(() => ({
3
+ mockApiGet: vi.fn(),
4
+ }));
5
+ vi.mock("../../core/api-client.js", () => ({
6
+ apiGet: (...args) => mockApiGet(...args),
7
+ }));
8
+ async function getRebateStatusTool() {
9
+ const tools = new Map();
10
+ const server = {
11
+ tool: (name, _description, _schema, handler) => {
12
+ tools.set(name, handler);
13
+ },
14
+ };
15
+ const { registerRebateTools } = await import("../rebates.js");
16
+ registerRebateTools(server);
17
+ const tool = tools.get("rebate_status");
18
+ if (!tool)
19
+ throw new Error("rebate_status tool was not registered");
20
+ return tool;
21
+ }
22
+ function flattenText(result) {
23
+ return result.content.map((item) => item.text).join("\n");
24
+ }
25
+ describe("rebate_status tool", () => {
26
+ beforeEach(() => {
27
+ vi.resetModules();
28
+ mockApiGet.mockReset();
29
+ });
30
+ it("formats rebate balances and recent payouts", async () => {
31
+ mockApiGet.mockResolvedValueOnce({
32
+ consumer_principal: "did:pkh:eip155:8453:0xabc",
33
+ principal_type: "evm",
34
+ principal_value: "0xabc",
35
+ payout_address: "0xabc",
36
+ payout_chain: "base",
37
+ auto_payout_threshold_usd: "5.000000",
38
+ totals: {
39
+ lifetime_earned_usd: "2.500000",
40
+ pending_usd: "1.250000",
41
+ paid_usd: "1.000000",
42
+ blocked_usd: "0.250000",
43
+ total_count: 5,
44
+ pending_count: 2,
45
+ paid_count: 2,
46
+ blocked_count: 1,
47
+ },
48
+ by_source: [
49
+ { source_type: "job_execution", status: "earned", count: 2, rebate_usd: "1.250000" },
50
+ ],
51
+ recent_payouts: [
52
+ {
53
+ id: "payout-1",
54
+ status: "completed",
55
+ amount_usd: "1.000000",
56
+ earning_count: 2,
57
+ tx_hash: "0xhash",
58
+ processed_at: "2026-05-09T12:00:00.000Z",
59
+ },
60
+ ],
61
+ });
62
+ const tool = await getRebateStatusTool();
63
+ const result = await tool({});
64
+ const text = flattenText(result);
65
+ expect(mockApiGet).toHaveBeenCalledWith("/rebates/status", { ensureConsumerPrincipal: true });
66
+ expect(text).toContain("Lifetime earned: $2.5000 across 5 rebate(s)");
67
+ expect(text).toContain("Pending: $1.2500 across 2 rebate(s)");
68
+ expect(text).toContain("Auto payout: $3.7500 until the $5.0000 threshold");
69
+ expect(text).toContain("job execution / earned: $1.2500 (2)");
70
+ expect(text).toContain("completed: $1.0000 (2) tx=0xhash");
71
+ });
72
+ });