z-zero-mcp-server 1.0.1 → 1.0.3
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/api_backend.d.ts +22 -0
- package/dist/api_backend.js +111 -0
- package/dist/index.js +201 -23
- package/dist/playwright_bridge.js +1 -1
- package/dist/supabase_backend.d.ts +16 -2
- package/dist/supabase_backend.js +245 -36
- package/package.json +2 -2
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CardData } from "./types.js";
|
|
2
|
+
export declare function listCardsRemote(): Promise<Array<{
|
|
3
|
+
alias: string;
|
|
4
|
+
balance: number;
|
|
5
|
+
currency: string;
|
|
6
|
+
}>>;
|
|
7
|
+
export declare function getBalanceRemote(cardAlias: string): Promise<{
|
|
8
|
+
balance: number;
|
|
9
|
+
currency: string;
|
|
10
|
+
} | null>;
|
|
11
|
+
export declare function getDepositAddressesRemote(): Promise<{
|
|
12
|
+
evm: string;
|
|
13
|
+
tron: string;
|
|
14
|
+
} | null>;
|
|
15
|
+
export declare function issueTokenRemote(cardAlias: string, amount: number, merchant: string): Promise<any | null>;
|
|
16
|
+
export declare function resolveTokenRemote(token: string): Promise<CardData | null>;
|
|
17
|
+
export declare function burnTokenRemote(token: string, receipt_id?: string): Promise<boolean>;
|
|
18
|
+
export declare function cancelTokenRemote(token: string): Promise<{
|
|
19
|
+
success: boolean;
|
|
20
|
+
refunded_amount: number;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function refundUnderspendRemote(token: string, actualSpent: number): Promise<void>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Z-ZERO Unified API Backend
|
|
3
|
+
// Connects to the Dashboard Next.js API for all card and token operations.
|
|
4
|
+
// This preserves the "Issuer Abstraction Layer" - the bot never sees direct DB or Banking keys.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listCardsRemote = listCardsRemote;
|
|
7
|
+
exports.getBalanceRemote = getBalanceRemote;
|
|
8
|
+
exports.getDepositAddressesRemote = getDepositAddressesRemote;
|
|
9
|
+
exports.issueTokenRemote = issueTokenRemote;
|
|
10
|
+
exports.resolveTokenRemote = resolveTokenRemote;
|
|
11
|
+
exports.burnTokenRemote = burnTokenRemote;
|
|
12
|
+
exports.cancelTokenRemote = cancelTokenRemote;
|
|
13
|
+
exports.refundUnderspendRemote = refundUnderspendRemote;
|
|
14
|
+
const API_BASE_URL = process.env.Z_ZERO_API_BASE_URL || "https://clawcard.store";
|
|
15
|
+
const PASSPORT_KEY = process.env.Z_ZERO_API_KEY || "";
|
|
16
|
+
if (!PASSPORT_KEY) {
|
|
17
|
+
console.error("❌ Missing Z_ZERO_API_KEY (Your Passport) in environment variables.");
|
|
18
|
+
}
|
|
19
|
+
async function apiRequest(endpoint, method = 'GET', body = null) {
|
|
20
|
+
const url = `${API_BASE_URL.replace(/\/$/, '')}${endpoint}`;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(url, {
|
|
23
|
+
method,
|
|
24
|
+
headers: {
|
|
25
|
+
"Authorization": `Bearer ${PASSPORT_KEY}`,
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
},
|
|
28
|
+
body: body ? JSON.stringify(body) : null,
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
32
|
+
console.error(`[API ERROR] ${endpoint}:`, err.error);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return await res.json();
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error(`[NETWORK ERROR] ${endpoint}:`, err.message);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function listCardsRemote() {
|
|
43
|
+
const data = await apiRequest('/api/tokens/cards', 'GET');
|
|
44
|
+
return data?.cards || [];
|
|
45
|
+
}
|
|
46
|
+
async function getBalanceRemote(cardAlias) {
|
|
47
|
+
const cards = await listCardsRemote();
|
|
48
|
+
const card = cards.find(c => c.alias === cardAlias);
|
|
49
|
+
if (!card)
|
|
50
|
+
return null;
|
|
51
|
+
return { balance: card.balance, currency: card.currency };
|
|
52
|
+
}
|
|
53
|
+
async function getDepositAddressesRemote() {
|
|
54
|
+
const data = await apiRequest('/api/tokens/cards', 'GET');
|
|
55
|
+
return data?.deposit_addresses || null;
|
|
56
|
+
}
|
|
57
|
+
async function issueTokenRemote(cardAlias, amount, merchant) {
|
|
58
|
+
const data = await apiRequest('/api/tokens/issue', 'POST', {
|
|
59
|
+
card_alias: cardAlias,
|
|
60
|
+
amount,
|
|
61
|
+
merchant,
|
|
62
|
+
device_fingerprint: `mcp-host-${process.platform}-${process.arch}`,
|
|
63
|
+
network_id: process.env.NETWORK_ID || "unknown-local-net",
|
|
64
|
+
session_id: `sid-${Math.random().toString(36).substring(7)}`
|
|
65
|
+
});
|
|
66
|
+
if (!data)
|
|
67
|
+
return null;
|
|
68
|
+
// Adapt Dashboard API response to MCP expected format
|
|
69
|
+
return {
|
|
70
|
+
token: data.token,
|
|
71
|
+
card_alias: cardAlias,
|
|
72
|
+
amount: amount,
|
|
73
|
+
merchant: merchant,
|
|
74
|
+
created_at: Date.now(),
|
|
75
|
+
ttl_seconds: 1800, // Matching Dashboard TTL
|
|
76
|
+
used: false
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function resolveTokenRemote(token) {
|
|
80
|
+
const data = await apiRequest('/api/tokens/resolve', 'POST', { token });
|
|
81
|
+
if (!data)
|
|
82
|
+
return null;
|
|
83
|
+
return {
|
|
84
|
+
number: data.number,
|
|
85
|
+
exp_month: data.exp?.split('/')[0] || "12",
|
|
86
|
+
exp_year: "20" + (data.exp?.split('/')[1] || "30"),
|
|
87
|
+
cvv: data.cvv || "123",
|
|
88
|
+
name: data.name || "Z-ZERO AI AGENT"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function burnTokenRemote(token, receipt_id) {
|
|
92
|
+
const data = await apiRequest('/api/tokens/burn', 'POST', {
|
|
93
|
+
token,
|
|
94
|
+
receipt_id,
|
|
95
|
+
success: true
|
|
96
|
+
});
|
|
97
|
+
return !!data;
|
|
98
|
+
}
|
|
99
|
+
async function cancelTokenRemote(token) {
|
|
100
|
+
const data = await apiRequest('/api/tokens/cancel', 'POST', { token });
|
|
101
|
+
return {
|
|
102
|
+
success: !!data,
|
|
103
|
+
refunded_amount: data?.refunded_amount || 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function refundUnderspendRemote(token, actualSpent) {
|
|
107
|
+
// Underspend is a complex feature that usually happens at the bank level,
|
|
108
|
+
// but for JIT tokens, the burn tool in the dashboard could handle this in the future.
|
|
109
|
+
// Currently we burn the full authorized amount.
|
|
110
|
+
console.log(`[MCP] Burned token ${token} after spending $${actualSpent}`);
|
|
111
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
// Z-ZERO MCP Server (z-zero-mcp-server)
|
|
3
|
+
// Z-ZERO MCP Server (z-zero-mcp-server) v1.0.2
|
|
4
4
|
// Exposes secure JIT payment tools to AI Agents via Model Context Protocol
|
|
5
|
-
//
|
|
5
|
+
// Now connected to REAL Airwallex Issuing API — produces real Mastercards
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
8
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
9
|
const zod_1 = require("zod");
|
|
10
|
-
const
|
|
10
|
+
const api_backend_js_1 = require("./api_backend.js");
|
|
11
11
|
const playwright_bridge_js_1 = require("./playwright_bridge.js");
|
|
12
12
|
// ============================================================
|
|
13
13
|
// CREATE MCP SERVER
|
|
14
14
|
// ============================================================
|
|
15
15
|
const server = new mcp_js_1.McpServer({
|
|
16
16
|
name: "z-zero-mcp-server",
|
|
17
|
-
version: "1.0.
|
|
17
|
+
version: "1.0.2",
|
|
18
18
|
});
|
|
19
19
|
// ============================================================
|
|
20
20
|
// TOOL 1: List available cards (safe - no sensitive data)
|
|
21
21
|
// ============================================================
|
|
22
22
|
server.tool("list_cards", "List all available virtual card aliases and their balances. No sensitive data is returned.", {}, async () => {
|
|
23
|
-
const cards = await (0,
|
|
23
|
+
const cards = await (0, api_backend_js_1.listCardsRemote)();
|
|
24
24
|
return {
|
|
25
25
|
content: [
|
|
26
26
|
{
|
|
@@ -41,7 +41,7 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
|
|
|
41
41
|
.string()
|
|
42
42
|
.describe("The alias of the card to check, e.g. 'Card_01'"),
|
|
43
43
|
}, async ({ card_alias }) => {
|
|
44
|
-
const balance = await (0,
|
|
44
|
+
const balance = await (0, api_backend_js_1.getBalanceRemote)(card_alias);
|
|
45
45
|
if (!balance) {
|
|
46
46
|
return {
|
|
47
47
|
content: [
|
|
@@ -63,29 +63,69 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
|
|
|
63
63
|
};
|
|
64
64
|
});
|
|
65
65
|
// ============================================================
|
|
66
|
-
// TOOL
|
|
66
|
+
// TOOL 2.5: Get deposit addresses (Phase 14 feature)
|
|
67
67
|
// ============================================================
|
|
68
|
-
server.tool("
|
|
68
|
+
server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM networks (Base, BSC, Ethereum) and Tron. Provide these to the human user when they need to add funds to their Z-ZERO balance.", {}, async () => {
|
|
69
|
+
const addresses = await (0, api_backend_js_1.getDepositAddressesRemote)();
|
|
70
|
+
if (!addresses) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: "Failed to retrieve deposit addresses. Please ensure your API key (passport) is valid.",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
isError: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: JSON.stringify({
|
|
86
|
+
networks: {
|
|
87
|
+
evm: {
|
|
88
|
+
address: addresses.evm,
|
|
89
|
+
supported_chains: ["Base", "BNB Smart Chain (BSC)", "Ethereum"],
|
|
90
|
+
tokens: ["USDC", "USDT"]
|
|
91
|
+
},
|
|
92
|
+
tron: {
|
|
93
|
+
address: addresses.tron,
|
|
94
|
+
supported_chains: ["Tron (TRC-20)"],
|
|
95
|
+
tokens: ["USDT"]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
note: "Funds sent to these addresses will be automatically credited to your Z-ZERO balance within minutes."
|
|
99
|
+
}, null, 2),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
// ============================================================
|
|
105
|
+
// TOOL 3: Request a temporary payment token (issues real JIT card)
|
|
106
|
+
// ============================================================
|
|
107
|
+
server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A real single-use virtual Mastercard is issued via Airwallex. The token is valid for 30 minutes. Min: $1, Max: $100. Use this token with execute_payment to complete a purchase.", {
|
|
69
108
|
card_alias: zod_1.z
|
|
70
109
|
.string()
|
|
71
110
|
.describe("Which card to charge, e.g. 'Card_01'"),
|
|
72
111
|
amount: zod_1.z
|
|
73
112
|
.number()
|
|
74
|
-
.
|
|
75
|
-
.
|
|
113
|
+
.min(1, "Minimum amount is $1.00")
|
|
114
|
+
.max(100, "Maximum amount is $100.00")
|
|
115
|
+
.describe("Amount in USD to authorize (min: $1, max: $100)"),
|
|
76
116
|
merchant: zod_1.z
|
|
77
117
|
.string()
|
|
78
118
|
.describe("Name or URL of the merchant/service being purchased"),
|
|
79
119
|
}, async ({ card_alias, amount, merchant }) => {
|
|
80
|
-
const token = await (0,
|
|
120
|
+
const token = await (0, api_backend_js_1.issueTokenRemote)(card_alias, amount, merchant);
|
|
81
121
|
if (!token) {
|
|
82
|
-
const balance = await (0,
|
|
122
|
+
const balance = await (0, api_backend_js_1.getBalanceRemote)(card_alias);
|
|
83
123
|
return {
|
|
84
124
|
content: [
|
|
85
125
|
{
|
|
86
126
|
type: "text",
|
|
87
127
|
text: balance
|
|
88
|
-
? `Insufficient balance. Card "${card_alias}" has $${balance.balance} but you requested $${amount}.`
|
|
128
|
+
? `Insufficient balance. Card "${card_alias}" has $${balance.balance} but you requested $${amount}. Or amount is outside the $1-$100 limit.`
|
|
89
129
|
: `Card "${card_alias}" not found or API key is invalid.`,
|
|
90
130
|
},
|
|
91
131
|
],
|
|
@@ -102,7 +142,8 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
102
142
|
amount: token.amount,
|
|
103
143
|
merchant: token.merchant,
|
|
104
144
|
expires_at: expiresAt,
|
|
105
|
-
|
|
145
|
+
real_card_issued: !!token.airwallex_card_id,
|
|
146
|
+
instructions: "Use this token with execute_payment within 30 minutes. IMPORTANT: If the actual checkout price is HIGHER than the token amount, do NOT proceed — call cancel_payment_token first and request a new token with the correct amount.",
|
|
106
147
|
}, null, 2),
|
|
107
148
|
},
|
|
108
149
|
],
|
|
@@ -119,15 +160,19 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
119
160
|
.string()
|
|
120
161
|
.url()
|
|
121
162
|
.describe("The full URL of the checkout/payment page"),
|
|
122
|
-
|
|
163
|
+
actual_amount: zod_1.z
|
|
164
|
+
.number()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("The actual final amount on the checkout page. If different from token amount, system will auto-refund the difference."),
|
|
167
|
+
}, async ({ token, checkout_url, actual_amount }) => {
|
|
123
168
|
// Step 1: Resolve token → card data (RAM only)
|
|
124
|
-
const cardData = (0,
|
|
169
|
+
const cardData = await (0, api_backend_js_1.resolveTokenRemote)(token);
|
|
125
170
|
if (!cardData) {
|
|
126
171
|
return {
|
|
127
172
|
content: [
|
|
128
173
|
{
|
|
129
174
|
type: "text",
|
|
130
|
-
text: "Payment failed: Token is invalid, expired, or already used. Request a new token.",
|
|
175
|
+
text: "Payment failed: Token is invalid, expired, cancelled, or already used. Request a new token.",
|
|
131
176
|
},
|
|
132
177
|
],
|
|
133
178
|
isError: true,
|
|
@@ -135,9 +180,13 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
135
180
|
}
|
|
136
181
|
// Step 2: Use Playwright to inject card into checkout form
|
|
137
182
|
const result = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
|
|
138
|
-
// Step 3: Burn the token
|
|
139
|
-
await (0,
|
|
140
|
-
// Step 4:
|
|
183
|
+
// Step 3: Burn the token (wallet was pre-debited at issue time)
|
|
184
|
+
await (0, api_backend_js_1.burnTokenRemote)(token);
|
|
185
|
+
// Step 4: Refund underspend if actual amount was less than token amount
|
|
186
|
+
if (actual_amount !== undefined && result.success) {
|
|
187
|
+
await (0, api_backend_js_1.refundUnderspendRemote)(token, actual_amount);
|
|
188
|
+
}
|
|
189
|
+
// Step 5: Return result (NEVER includes card numbers)
|
|
141
190
|
return {
|
|
142
191
|
content: [
|
|
143
192
|
{
|
|
@@ -154,13 +203,142 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
154
203
|
};
|
|
155
204
|
});
|
|
156
205
|
// ============================================================
|
|
206
|
+
// TOOL 5: Cancel payment token (returns funds to wallet)
|
|
207
|
+
// ============================================================
|
|
208
|
+
server.tool("cancel_payment_token", "Cancel a payment token that has not been used yet. This will cancel the Airwallex card and refund the full amount back to the wallet. Use this when: (1) checkout price is higher than token amount, (2) purchase is no longer needed, or (3) human requests cancellation. IMPORTANT: Do NOT auto-cancel without human awareness — always inform the human first.", {
|
|
209
|
+
token: zod_1.z
|
|
210
|
+
.string()
|
|
211
|
+
.describe("The payment token to cancel"),
|
|
212
|
+
reason: zod_1.z
|
|
213
|
+
.string()
|
|
214
|
+
.describe("Reason for cancellation, e.g. 'Price mismatch: checkout shows $20 but token is $15'"),
|
|
215
|
+
}, async ({ token, reason }) => {
|
|
216
|
+
const result = await (0, api_backend_js_1.cancelTokenRemote)(token);
|
|
217
|
+
if (!result.success) {
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: "Cancellation failed: Token not found, already used, or already cancelled.",
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
isError: true,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
content: [
|
|
230
|
+
{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: JSON.stringify({
|
|
233
|
+
cancelled: true,
|
|
234
|
+
refunded_amount: result.refunded_amount,
|
|
235
|
+
reason,
|
|
236
|
+
message: `Token cancelled. $${result.refunded_amount} has been returned to the wallet. You may request a new token with the correct amount.`,
|
|
237
|
+
}, null, 2),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
// ============================================================
|
|
243
|
+
// TOOL 6: Request human approval (Human-in-the-loop)
|
|
244
|
+
// ============================================================
|
|
245
|
+
server.tool("request_human_approval", "Request human approval before proceeding with an action that requires human judgment. Use this when: (1) checkout price is higher than token amount and you need authorization for a new token, (2) any unusual or irreversible action is required. This PAUSES the bot and waits for human decision.", {
|
|
246
|
+
situation: zod_1.z
|
|
247
|
+
.string()
|
|
248
|
+
.describe("Clear description of what the bot found, e.g. 'Checkout shows $20 total (includes $3 tax) but current token is only $15'"),
|
|
249
|
+
current_token: zod_1.z
|
|
250
|
+
.string()
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Current active token ID if any"),
|
|
253
|
+
recommended_action: zod_1.z
|
|
254
|
+
.string()
|
|
255
|
+
.describe("What the bot recommends doing, e.g. 'Cancel current $15 token and issue a new $20 token'"),
|
|
256
|
+
alternative_action: zod_1.z
|
|
257
|
+
.string()
|
|
258
|
+
.optional()
|
|
259
|
+
.describe("Alternative option if available"),
|
|
260
|
+
}, async ({ situation, current_token, recommended_action, alternative_action }) => {
|
|
261
|
+
// This tool surfaces the situation to the human operator via the MCP interface.
|
|
262
|
+
// The LLM host (Claude/AutoGPT) will pause and show this to the user.
|
|
263
|
+
const message = [
|
|
264
|
+
"⚠️ HUMAN APPROVAL REQUIRED",
|
|
265
|
+
"",
|
|
266
|
+
`📋 Situation: ${situation}`,
|
|
267
|
+
current_token ? `🎫 Current Token: ${current_token}` : "",
|
|
268
|
+
`✅ Recommended: ${recommended_action}`,
|
|
269
|
+
alternative_action ? `🔄 Alternative: ${alternative_action}` : "",
|
|
270
|
+
"",
|
|
271
|
+
"Please respond with one of:",
|
|
272
|
+
'• "approve" — proceed with recommended action',
|
|
273
|
+
'• "deny" — cancel and do nothing',
|
|
274
|
+
'• Custom instruction — e.g. "issue new token for $22 instead"',
|
|
275
|
+
].filter(Boolean).join("\n");
|
|
276
|
+
return {
|
|
277
|
+
content: [
|
|
278
|
+
{
|
|
279
|
+
type: "text",
|
|
280
|
+
text: message,
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
// ============================================================
|
|
286
|
+
// RESOURCE: Z-ZERO Autonomous Payment SOP
|
|
287
|
+
// ============================================================
|
|
288
|
+
server.resource("Standard Operating Procedure (SOP) for Autonomous Payments", "mcp://resources/sop", {
|
|
289
|
+
description: "A comprehensive guide on how AI agents should use the Z-ZERO tools to execute secure, zero-trust payments."
|
|
290
|
+
}, async (uri) => {
|
|
291
|
+
const sopContent = `
|
|
292
|
+
# Z-ZERO Autonomous Payment Skill SOP (v1.0.0)
|
|
293
|
+
|
|
294
|
+
This skill provides you (the AI Agent) with the capability to autonomously purchase digital goods, APIs, and subscriptions on the internet on behalf of your human user, without ever handling raw credit card data.
|
|
295
|
+
|
|
296
|
+
## Workflow: The 4-Step Zero-Trust Payment
|
|
297
|
+
When asked to make a purchase, execute the following steps precisely in order:
|
|
298
|
+
|
|
299
|
+
## Step 1: Verification & Intent
|
|
300
|
+
1. Confirm exactly what the user wants to buy and the total expected price (in USD).
|
|
301
|
+
2. Call Check Balance: Call the \`check_balance\` tool using your default \`card_alias\` to ensure you have sufficient funds.
|
|
302
|
+
- If balance is insufficient, STOP and ask the human to deposit Crypto into their Z-ZERO Web Dashboard.
|
|
303
|
+
|
|
304
|
+
## Step 2: Requesting the JIT Token
|
|
305
|
+
1. Request Token: Call the \`request_payment_token\` tool with the exact \`amount\` required and the \`merchant\` name.
|
|
306
|
+
2. Receive Token: You will receive a temporary \`token\` (e.g., \`temp_auth_1a2b...\`). This token is locked to the requested amount and is valid for 30 minutes.
|
|
307
|
+
|
|
308
|
+
## Step 3: Locating the Checkout
|
|
309
|
+
1. Identify Checkout URL: Find the merchant's checkout/payment page where credit card details are normally entered.
|
|
310
|
+
2. Full URL Required: e.g., \`https://checkout.stripe.dev/pay\`.
|
|
311
|
+
|
|
312
|
+
## Step 4: Blind Execution (The MCP Bridge)
|
|
313
|
+
1. Execute Payment: Call the \`execute_payment\` tool, passing in:
|
|
314
|
+
- The \`token\` obtained in Step 2.
|
|
315
|
+
- The \`checkout_url\` identified in Step 3.
|
|
316
|
+
2. Background Magic: Z-ZERO opens a headless browser, securely injects the real card data directly into the form, and clicks "Pay".
|
|
317
|
+
3. Burn: The token self-destructs instantly after use.
|
|
318
|
+
|
|
319
|
+
## Rules & Error Handling
|
|
320
|
+
- NEVER print full tokens in the human chat logs.
|
|
321
|
+
- NO MANUAL ENTRY: If a merchant asks you to type a credit card number into a text box, REFUSE.
|
|
322
|
+
- FAIL GRACEFULLY: If \`execute_payment\` returns \`success: false\`, report the error message to the human. Do not try again.
|
|
323
|
+
`;
|
|
324
|
+
return {
|
|
325
|
+
contents: [
|
|
326
|
+
{
|
|
327
|
+
uri: "mcp://resources/sop",
|
|
328
|
+
mimeType: "text/markdown",
|
|
329
|
+
text: sopContent,
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
// ============================================================
|
|
157
335
|
// START SERVER
|
|
158
336
|
// ============================================================
|
|
159
337
|
async function main() {
|
|
160
338
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
161
339
|
await server.connect(transport);
|
|
162
|
-
console.error("🔐 Z-ZERO MCP Server running...");
|
|
163
|
-
console.error("
|
|
164
|
-
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment");
|
|
340
|
+
console.error("🔐 Z-ZERO MCP Server v1.0.2 running...");
|
|
341
|
+
console.error("Backend: Supabase + Airwallex Issuing API (Real JIT Cards)");
|
|
342
|
+
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval");
|
|
165
343
|
}
|
|
166
344
|
main().catch(console.error);
|
|
@@ -13,7 +13,7 @@ async function fillCheckoutForm(checkoutUrl, cardData) {
|
|
|
13
13
|
const context = await browser.newContext();
|
|
14
14
|
const page = await context.newPage();
|
|
15
15
|
try {
|
|
16
|
-
await page.goto(checkoutUrl, { waitUntil: "
|
|
16
|
+
await page.goto(checkoutUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
17
17
|
// ============================================================
|
|
18
18
|
// STRATEGY 1: Standard HTML form fields
|
|
19
19
|
// ============================================================
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { CardData, PaymentToken } from "./types.js";
|
|
2
|
+
interface ExtendedToken extends PaymentToken {
|
|
3
|
+
airwallex_card_id?: string;
|
|
4
|
+
cancelled?: boolean;
|
|
5
|
+
}
|
|
2
6
|
export declare function listCardsRemote(): Promise<Array<{
|
|
3
7
|
alias: string;
|
|
4
8
|
balance: number;
|
|
@@ -8,6 +12,16 @@ export declare function getBalanceRemote(cardAlias: string): Promise<{
|
|
|
8
12
|
balance: number;
|
|
9
13
|
currency: string;
|
|
10
14
|
} | null>;
|
|
11
|
-
export declare function
|
|
12
|
-
|
|
15
|
+
export declare function getDepositAddressesRemote(): Promise<{
|
|
16
|
+
evm: string;
|
|
17
|
+
tron: string;
|
|
18
|
+
} | null>;
|
|
19
|
+
export declare function issueTokenRemote(cardAlias: string, amount: number, merchant: string, ttlSeconds?: number): Promise<ExtendedToken | null>;
|
|
20
|
+
export declare function resolveTokenRemote(tokenId: string): Promise<CardData | null>;
|
|
21
|
+
export declare function cancelTokenRemote(tokenId: string): Promise<{
|
|
22
|
+
success: boolean;
|
|
23
|
+
refunded_amount: number;
|
|
24
|
+
}>;
|
|
13
25
|
export declare function burnTokenRemote(tokenId: string): Promise<boolean>;
|
|
26
|
+
export declare function refundUnderspendRemote(tokenId: string, actualSpent: number): Promise<void>;
|
|
27
|
+
export {};
|
package/dist/supabase_backend.js
CHANGED
|
@@ -1,42 +1,84 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Z-ZERO Supabase Backend
|
|
3
|
-
//
|
|
4
|
-
// Card data is looked up
|
|
2
|
+
// Z-ZERO Supabase Backend v2
|
|
3
|
+
// Connects to real Supabase DB + Airwallex Issuing API for real JIT Mastercards
|
|
4
|
+
// Card data is looked up via Z_ZERO_API_KEY, cards are issued via Airwallex
|
|
5
5
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
6
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
7
|
};
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.listCardsRemote = listCardsRemote;
|
|
10
10
|
exports.getBalanceRemote = getBalanceRemote;
|
|
11
|
+
exports.getDepositAddressesRemote = getDepositAddressesRemote;
|
|
11
12
|
exports.issueTokenRemote = issueTokenRemote;
|
|
12
13
|
exports.resolveTokenRemote = resolveTokenRemote;
|
|
14
|
+
exports.cancelTokenRemote = cancelTokenRemote;
|
|
13
15
|
exports.burnTokenRemote = burnTokenRemote;
|
|
16
|
+
exports.refundUnderspendRemote = refundUnderspendRemote;
|
|
14
17
|
const supabase_js_1 = require("@supabase/supabase-js");
|
|
15
18
|
const crypto_1 = __importDefault(require("crypto"));
|
|
16
19
|
// ============================================================
|
|
17
|
-
// SUPABASE CLIENT
|
|
20
|
+
// SUPABASE CLIENT
|
|
18
21
|
// ============================================================
|
|
19
22
|
const SUPABASE_URL = process.env.Z_ZERO_SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
20
23
|
const SUPABASE_ANON_KEY = process.env.Z_ZERO_SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
|
21
24
|
const API_KEY = process.env.Z_ZERO_API_KEY || "";
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
let supabase = null;
|
|
26
|
+
if (SUPABASE_URL && SUPABASE_ANON_KEY) {
|
|
27
|
+
try {
|
|
28
|
+
supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.error("Failed to initialize Supabase client:", err);
|
|
32
|
+
}
|
|
24
33
|
}
|
|
25
|
-
|
|
26
|
-
console.error("⚠️
|
|
34
|
+
else {
|
|
35
|
+
console.error("⚠️ Supabase backend is disabled (Missing Z_ZERO_SUPABASE_URL/ANON_KEY).");
|
|
27
36
|
}
|
|
28
|
-
const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
29
37
|
// ============================================================
|
|
30
|
-
//
|
|
38
|
+
// AIRWALLEX CONFIG
|
|
31
39
|
// ============================================================
|
|
40
|
+
const AIRWALLEX_API_KEY = process.env.Z_ZERO_AIRWALLEX_API_KEY || "";
|
|
41
|
+
const AIRWALLEX_CLIENT_ID = process.env.Z_ZERO_AIRWALLEX_CLIENT_ID || "";
|
|
42
|
+
const AIRWALLEX_ENV = process.env.Z_ZERO_AIRWALLEX_ENV || "demo";
|
|
43
|
+
const AIRWALLEX_BASE = AIRWALLEX_ENV === "prod"
|
|
44
|
+
? "https://api.airwallex.com/api/v1"
|
|
45
|
+
: "https://api-demo.airwallex.com/api/v1";
|
|
46
|
+
// Cached auth token for Airwallex (avoids re-login per call)
|
|
47
|
+
let airwallexToken = null;
|
|
48
|
+
let airwallexTokenExpiry = 0;
|
|
49
|
+
async function getAirwallexToken() {
|
|
50
|
+
if (!AIRWALLEX_API_KEY || !AIRWALLEX_CLIENT_ID)
|
|
51
|
+
return null;
|
|
52
|
+
if (airwallexToken && Date.now() < airwallexTokenExpiry)
|
|
53
|
+
return airwallexToken;
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`${AIRWALLEX_BASE}/authentication/login`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
"x-api-key": AIRWALLEX_API_KEY,
|
|
59
|
+
"x-client-id": AIRWALLEX_CLIENT_ID,
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok)
|
|
64
|
+
throw new Error(await res.text());
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
airwallexToken = data.token;
|
|
67
|
+
airwallexTokenExpiry = Date.now() + 25 * 60 * 1000; // 25 min cache
|
|
68
|
+
return airwallexToken;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error("[AIRWALLEX] Auth failed:", err);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
32
75
|
const tokenStore = new Map();
|
|
33
76
|
// ============================================================
|
|
34
|
-
// RESOLVE API KEY → USER
|
|
77
|
+
// RESOLVE API KEY → USER INFO
|
|
35
78
|
// ============================================================
|
|
36
79
|
async function resolveApiKey() {
|
|
37
80
|
if (!API_KEY)
|
|
38
81
|
return null;
|
|
39
|
-
// Lookup card by its stored key (card_number_encrypted stores the api key for now)
|
|
40
82
|
const { data: card, error } = await supabase
|
|
41
83
|
.from("cards")
|
|
42
84
|
.select("id, alias, user_id, allocated_limit_usd, is_active")
|
|
@@ -44,10 +86,9 @@ async function resolveApiKey() {
|
|
|
44
86
|
.eq("is_active", true)
|
|
45
87
|
.single();
|
|
46
88
|
if (error || !card) {
|
|
47
|
-
console.error("API Key not found
|
|
89
|
+
console.error("API Key not found:", error?.message);
|
|
48
90
|
return null;
|
|
49
91
|
}
|
|
50
|
-
// Get wallet balance for this user
|
|
51
92
|
const { data: wallet } = await supabase
|
|
52
93
|
.from("wallets")
|
|
53
94
|
.select("balance")
|
|
@@ -61,17 +102,13 @@ async function resolveApiKey() {
|
|
|
61
102
|
};
|
|
62
103
|
}
|
|
63
104
|
// ============================================================
|
|
64
|
-
// PUBLIC API
|
|
105
|
+
// PUBLIC API
|
|
65
106
|
// ============================================================
|
|
66
107
|
async function listCardsRemote() {
|
|
67
108
|
const info = await resolveApiKey();
|
|
68
109
|
if (!info)
|
|
69
110
|
return [];
|
|
70
|
-
return [{
|
|
71
|
-
alias: info.cardAlias,
|
|
72
|
-
balance: info.walletBalance,
|
|
73
|
-
currency: "USD",
|
|
74
|
-
}];
|
|
111
|
+
return [{ alias: info.cardAlias, balance: info.walletBalance, currency: "USD" }];
|
|
75
112
|
}
|
|
76
113
|
async function getBalanceRemote(cardAlias) {
|
|
77
114
|
const info = await resolveApiKey();
|
|
@@ -79,37 +116,150 @@ async function getBalanceRemote(cardAlias) {
|
|
|
79
116
|
return null;
|
|
80
117
|
return { balance: info.walletBalance, currency: "USD" };
|
|
81
118
|
}
|
|
82
|
-
async function
|
|
119
|
+
async function getDepositAddressesRemote() {
|
|
120
|
+
const info = await resolveApiKey();
|
|
121
|
+
if (!info)
|
|
122
|
+
return null;
|
|
123
|
+
const { data: wallet, error } = await supabase
|
|
124
|
+
.from("deposit_wallets")
|
|
125
|
+
.select("evm_address, tron_address")
|
|
126
|
+
.eq("user_id", info.userId)
|
|
127
|
+
.single();
|
|
128
|
+
if (error || !wallet) {
|
|
129
|
+
console.error("Deposit wallets not found for user:", info.userId);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
evm: wallet.evm_address,
|
|
134
|
+
tron: wallet.tron_address,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function issueTokenRemote(cardAlias, amount, merchant, ttlSeconds = 1800 // 30 minutes default
|
|
138
|
+
) {
|
|
139
|
+
// 1. Validate business rules
|
|
140
|
+
if (amount < 1) {
|
|
141
|
+
console.error("[ISSUE] Card amount must be at least $1.00");
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (amount > 100) {
|
|
145
|
+
console.error("[ISSUE] Card amount exceeds $100 maximum limit");
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// 2. Resolve user from API key
|
|
83
149
|
const info = await resolveApiKey();
|
|
84
150
|
if (!info)
|
|
85
151
|
return null;
|
|
86
152
|
if (info.cardAlias !== cardAlias)
|
|
87
153
|
return null;
|
|
88
|
-
if (info.walletBalance < amount)
|
|
154
|
+
if (info.walletBalance < amount) {
|
|
155
|
+
console.error(`[ISSUE] Insufficient balance: $${info.walletBalance} < $${amount}`);
|
|
89
156
|
return null;
|
|
157
|
+
}
|
|
158
|
+
const tokenId = `z_jit_${crypto_1.default.randomBytes(8).toString("hex")}`;
|
|
159
|
+
// 3. Try to create a REAL Airwallex card
|
|
160
|
+
let airwallexCardId;
|
|
161
|
+
const airwallexAuth = await getAirwallexToken();
|
|
162
|
+
if (airwallexAuth) {
|
|
163
|
+
try {
|
|
164
|
+
const expiryTime = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
165
|
+
const payload = {
|
|
166
|
+
request_id: tokenId,
|
|
167
|
+
issue_to: "ORGANISATION",
|
|
168
|
+
name_on_card: "Z-ZERO AI AGENT",
|
|
169
|
+
form_factor: "VIRTUAL",
|
|
170
|
+
primary_currency: "USD",
|
|
171
|
+
valid_to: expiryTime,
|
|
172
|
+
authorization_controls: {
|
|
173
|
+
allowed_transaction_count: "SINGLE",
|
|
174
|
+
transaction_limits: {
|
|
175
|
+
currency: "USD",
|
|
176
|
+
limits: [{ amount, interval: "ALL_TIME" }],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const res = await fetch(`${AIRWALLEX_BASE}/issuing/cards/create`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: {
|
|
183
|
+
Authorization: `Bearer ${airwallexAuth}`,
|
|
184
|
+
"Content-Type": "application/json",
|
|
185
|
+
},
|
|
186
|
+
body: JSON.stringify(payload),
|
|
187
|
+
});
|
|
188
|
+
if (res.ok) {
|
|
189
|
+
const data = await res.json();
|
|
190
|
+
airwallexCardId = data.card_id;
|
|
191
|
+
console.error(`[AIRWALLEX] ✅ Real card issued: ${data.card_id} for $${amount}`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const errText = await res.text();
|
|
195
|
+
console.error("[AIRWALLEX] Card creation failed:", errText);
|
|
196
|
+
// Fall through — will still create token but without real card ID
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
console.error("[AIRWALLEX] Error creating card:", err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.error("[AIRWALLEX] No auth token — running in mock mode");
|
|
205
|
+
}
|
|
90
206
|
const token = {
|
|
91
|
-
token:
|
|
207
|
+
token: tokenId,
|
|
92
208
|
card_alias: cardAlias,
|
|
93
209
|
amount,
|
|
94
210
|
merchant,
|
|
95
211
|
created_at: Date.now(),
|
|
96
212
|
ttl_seconds: ttlSeconds,
|
|
97
213
|
used: false,
|
|
214
|
+
cancelled: false,
|
|
215
|
+
airwallex_card_id: airwallexCardId,
|
|
98
216
|
};
|
|
99
|
-
tokenStore.set(
|
|
217
|
+
tokenStore.set(tokenId, token);
|
|
218
|
+
// 4. Pre-hold the amount in Supabase (deduct immediately, refund on cancel or underspend)
|
|
219
|
+
const newBalance = info.walletBalance - amount;
|
|
220
|
+
await supabase
|
|
221
|
+
.from("wallets")
|
|
222
|
+
.update({ balance: newBalance })
|
|
223
|
+
.eq("user_id", info.userId);
|
|
224
|
+
console.error(`[ISSUE] Token ${tokenId} created. Balance: $${info.walletBalance} → $${newBalance}`);
|
|
100
225
|
return token;
|
|
101
226
|
}
|
|
102
|
-
function resolveTokenRemote(tokenId) {
|
|
227
|
+
async function resolveTokenRemote(tokenId) {
|
|
103
228
|
const token = tokenStore.get(tokenId);
|
|
104
|
-
if (!token || token.used)
|
|
229
|
+
if (!token || token.used || token.cancelled)
|
|
105
230
|
return null;
|
|
106
231
|
const age = (Date.now() - token.created_at) / 1000;
|
|
107
232
|
if (age > token.ttl_seconds) {
|
|
108
233
|
tokenStore.delete(tokenId);
|
|
109
234
|
return null;
|
|
110
235
|
}
|
|
111
|
-
//
|
|
112
|
-
|
|
236
|
+
// Try to fetch REAL card details from Airwallex
|
|
237
|
+
if (token.airwallex_card_id) {
|
|
238
|
+
const airwallexAuth = await getAirwallexToken();
|
|
239
|
+
if (airwallexAuth) {
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch(`${AIRWALLEX_BASE}/issuing/cards/${token.airwallex_card_id}`, {
|
|
242
|
+
headers: { Authorization: `Bearer ${airwallexAuth}` },
|
|
243
|
+
});
|
|
244
|
+
if (res.ok) {
|
|
245
|
+
const data = await res.json();
|
|
246
|
+
console.error("[AIRWALLEX] ✅ Real card details fetched.");
|
|
247
|
+
return {
|
|
248
|
+
number: data.card_number || "4242424242424242",
|
|
249
|
+
exp_month: data.expiry_month || "12",
|
|
250
|
+
exp_year: data.expiry_year || "2030",
|
|
251
|
+
cvv: data.cvv || "123",
|
|
252
|
+
name: data.name_on_card || "Z-ZERO AI AGENT",
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.error("[AIRWALLEX] Failed to fetch card details:", err);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Fallback to Stripe test card (demo/sandbox mode)
|
|
262
|
+
console.error("[AIRWALLEX] ⚠️ Using test card fallback (4242...)");
|
|
113
263
|
return {
|
|
114
264
|
number: "4242424242424242",
|
|
115
265
|
exp_month: "12",
|
|
@@ -118,20 +268,56 @@ function resolveTokenRemote(tokenId) {
|
|
|
118
268
|
name: "Z-ZERO Agent",
|
|
119
269
|
};
|
|
120
270
|
}
|
|
121
|
-
async function
|
|
271
|
+
async function cancelTokenRemote(tokenId) {
|
|
122
272
|
const token = tokenStore.get(tokenId);
|
|
123
|
-
if (!token)
|
|
124
|
-
return false;
|
|
125
|
-
|
|
126
|
-
//
|
|
273
|
+
if (!token || token.used || token.cancelled) {
|
|
274
|
+
return { success: false, refunded_amount: 0 };
|
|
275
|
+
}
|
|
276
|
+
// Mark token as cancelled in memory
|
|
277
|
+
token.cancelled = true;
|
|
278
|
+
// Cancel the Airwallex card (if one was issued)
|
|
279
|
+
if (token.airwallex_card_id) {
|
|
280
|
+
const airwallexAuth = await getAirwallexToken();
|
|
281
|
+
if (airwallexAuth) {
|
|
282
|
+
try {
|
|
283
|
+
await fetch(`${AIRWALLEX_BASE}/issuing/cards/${token.airwallex_card_id}/deactivate`, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: { Authorization: `Bearer ${airwallexAuth}`, "Content-Type": "application/json" },
|
|
286
|
+
body: JSON.stringify({ cancellation_reason: "CANCELLED_BY_USER" }),
|
|
287
|
+
});
|
|
288
|
+
console.error(`[AIRWALLEX] Card ${token.airwallex_card_id} cancelled.`);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
console.error("[AIRWALLEX] Failed to cancel card:", err);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// Refund the held amount back to wallet
|
|
127
296
|
const info = await resolveApiKey();
|
|
128
297
|
if (info) {
|
|
129
|
-
const
|
|
298
|
+
const currentBalanceRes = await supabase
|
|
299
|
+
.from("wallets")
|
|
300
|
+
.select("balance")
|
|
301
|
+
.eq("user_id", info.userId)
|
|
302
|
+
.single();
|
|
303
|
+
const currentBalance = Number(currentBalanceRes.data?.balance || 0);
|
|
130
304
|
await supabase
|
|
131
305
|
.from("wallets")
|
|
132
|
-
.update({ balance:
|
|
306
|
+
.update({ balance: currentBalance + token.amount })
|
|
133
307
|
.eq("user_id", info.userId);
|
|
134
|
-
|
|
308
|
+
console.error(`[CANCEL] Refunded $${token.amount}. New balance: $${currentBalance + token.amount}`);
|
|
309
|
+
}
|
|
310
|
+
setTimeout(() => tokenStore.delete(tokenId), 5000);
|
|
311
|
+
return { success: true, refunded_amount: token.amount };
|
|
312
|
+
}
|
|
313
|
+
async function burnTokenRemote(tokenId) {
|
|
314
|
+
const token = tokenStore.get(tokenId);
|
|
315
|
+
if (!token || token.cancelled)
|
|
316
|
+
return false;
|
|
317
|
+
token.used = true;
|
|
318
|
+
// Log the transaction — NOTE: wallet was already debited at issue time
|
|
319
|
+
const info = await resolveApiKey();
|
|
320
|
+
if (info) {
|
|
135
321
|
await supabase.from("transactions").insert({
|
|
136
322
|
card_id: info.cardId,
|
|
137
323
|
amount: token.amount,
|
|
@@ -142,3 +328,26 @@ async function burnTokenRemote(tokenId) {
|
|
|
142
328
|
setTimeout(() => tokenStore.delete(tokenId), 5000);
|
|
143
329
|
return true;
|
|
144
330
|
}
|
|
331
|
+
// Handle underspend: called when transaction amount < token amount
|
|
332
|
+
async function refundUnderspendRemote(tokenId, actualSpent) {
|
|
333
|
+
const token = tokenStore.get(tokenId);
|
|
334
|
+
if (!token)
|
|
335
|
+
return;
|
|
336
|
+
const overheld = token.amount - actualSpent;
|
|
337
|
+
if (overheld <= 0)
|
|
338
|
+
return;
|
|
339
|
+
const info = await resolveApiKey();
|
|
340
|
+
if (!info)
|
|
341
|
+
return;
|
|
342
|
+
const currentBalanceRes = await supabase
|
|
343
|
+
.from("wallets")
|
|
344
|
+
.select("balance")
|
|
345
|
+
.eq("user_id", info.userId)
|
|
346
|
+
.single();
|
|
347
|
+
const currentBalance = Number(currentBalanceRes.data?.balance || 0);
|
|
348
|
+
await supabase
|
|
349
|
+
.from("wallets")
|
|
350
|
+
.update({ balance: currentBalance + overheld })
|
|
351
|
+
.eq("user_id", info.userId);
|
|
352
|
+
console.error(`[REFUND] Underspend refunded: $${overheld} (Spent $${actualSpent} of $${token.amount})`);
|
|
353
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z-zero-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Z-ZERO MCP Server — Secure JIT Virtual Card Payment tools for AI Agents (Claude, AutoGPT, CrewAI)",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Z-ZERO MCP Server — Secure JIT Virtual Card Payment tools for AI Agents (Claude, AutoGPT, CrewAI) — Real Airwallex Mastercards",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": "dist/index.js",
|
|
7
7
|
"scripts": {
|