z-zero-mcp-server 1.0.3 → 1.0.4
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/README.md +56 -29
- package/dist/api_backend.d.ts +4 -17
- package/dist/api_backend.js +17 -9
- package/dist/index.js +88 -22
- package/dist/types.d.ts +6 -0
- package/package.json +5 -5
- package/dist/supabase_backend.d.ts +0 -27
- package/dist/supabase_backend.js +0 -353
package/README.md
CHANGED
|
@@ -1,53 +1,80 @@
|
|
|
1
|
-
# Z-ZERO
|
|
1
|
+
# OpenClaw: Z-ZERO AI Agent MCP Server
|
|
2
2
|
|
|
3
|
-
A Zero-Trust Payment Protocol built specifically for AI Agents utilizing the Model Context Protocol (MCP).
|
|
3
|
+
A Zero-Trust Payment Protocol built specifically for AI Agents utilizing the Model Context Protocol (MCP). Use this to give your local agents (Claude, Cursor, AutoGPT) the power to make real-world purchases securely.
|
|
4
4
|
|
|
5
5
|
## The Concept (Tokenized JIT Payments)
|
|
6
6
|
This MCP server acts as an "Invisible Bridge" for your AI Agents. Instead of giving your LLM direct access to a 16-digit credit card number (which triggers safety filters and risks prompt injection theft), the AI only handles **temporary, single-use tokens**.
|
|
7
7
|
|
|
8
|
-
The local MCP client resolves the token securely in RAM, injects the real card data directly into the payment form (via Playwright
|
|
9
|
-
|
|
10
|
-
## Features
|
|
11
|
-
- **Zero PII (Blind Execution):** AI never sees card numbers.
|
|
12
|
-
- **Exact-Match Funding:** Tokens are tied to virtual cards locked to the exact requested amount.
|
|
13
|
-
- **Phantom Burn:** Single-use architecture. Cards self-destruct after checking out.
|
|
14
|
-
- **Stripe/HTML Form Injection:** Automatically fills common checkout domains.
|
|
15
|
-
|
|
16
|
-
## Documentation
|
|
17
|
-
Check the `docs/` folder for complete system design and architecture:
|
|
18
|
-
- [Product Plan & Core Concept](docs/ai_card_product_plan.md)
|
|
19
|
-
- [MCP Implementation Plan](docs/implementation_plan.md)
|
|
20
|
-
- [Mock Backend Architecture](docs/backend_architecture.md)
|
|
21
|
-
- [Live Demo Walkthrough & Test Results](docs/walkthrough.md)
|
|
8
|
+
The local MCP client resolves the token securely in RAM, injects the real card data directly into the payment form (via Playwright), and clicks "Pay". The virtual card and token are burned milliseconds later.
|
|
22
9
|
|
|
23
10
|
## Getting Started
|
|
24
11
|
|
|
25
|
-
### 1.
|
|
12
|
+
### 1. Requirements
|
|
13
|
+
- **Node.js** (v18+)
|
|
14
|
+
- **Git**
|
|
15
|
+
- **Z-ZERO Passport Key** (Get yours at [clawcard.store/dashboard/agents](https://clawcard.store/dashboard/agents))
|
|
16
|
+
|
|
17
|
+
### 2. Installation
|
|
26
18
|
```bash
|
|
19
|
+
git clone https://github.com/openclaw/z-zero-mcp-server.git
|
|
20
|
+
cd z-zero-mcp-server
|
|
27
21
|
npm install
|
|
28
22
|
npx playwright install chromium
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### 2. Build the Server
|
|
32
|
-
```bash
|
|
33
23
|
npm run build
|
|
34
24
|
```
|
|
35
25
|
|
|
36
|
-
### 3.
|
|
37
|
-
|
|
26
|
+
### 3. Integration into AI Tools
|
|
27
|
+
|
|
28
|
+
#### Claude Desktop
|
|
29
|
+
Add the following to your `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
30
|
+
|
|
38
31
|
```json
|
|
39
32
|
{
|
|
40
33
|
"mcpServers": {
|
|
41
|
-
"
|
|
34
|
+
"openclaw": {
|
|
42
35
|
"command": "node",
|
|
43
|
-
"args": ["/
|
|
36
|
+
"args": ["/ABSOLUTE/PATH/TO/z-zero-mcp-server/dist/index.js"],
|
|
37
|
+
"env": {
|
|
38
|
+
"Z_ZERO_API_KEY": "your_passport_key_here",
|
|
39
|
+
"Z_ZERO_API_BASE_URL": "https://clawcard.store"
|
|
40
|
+
}
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
```
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
#### Cursor / IDEs
|
|
47
|
+
1. Go to **Settings** > **Cursor Settings** > **Features** > **MCP**.
|
|
48
|
+
2. Click **+ Add New MCP Server**.
|
|
49
|
+
3. Name: `openclaw`
|
|
50
|
+
4. Type: `command`
|
|
51
|
+
5. Command: `env Z_ZERO_API_KEY=your_passport_key_here node /ABSOLUTE/PATH/TO/z-zero-mcp-server/dist/index.js`
|
|
52
|
+
|
|
53
|
+
## Available Tools
|
|
54
|
+
- `list_cards`: View available virtual card aliases and balances.
|
|
51
55
|
- `check_balance`: Query a specific card's real-time balance.
|
|
52
|
-
- `request_payment_token`: Generate a JIT auth token
|
|
53
|
-
- `execute_payment`:
|
|
56
|
+
- `request_payment_token`: Generate a JIT auth token for a specific amount.
|
|
57
|
+
- `execute_payment`: Securely auto-fills checkout URLs and executes the payment.
|
|
58
|
+
- `get_deposit_addresses`: Get your unique addresses to top up your balance.
|
|
59
|
+
|
|
60
|
+
## Troubleshooting
|
|
61
|
+
|
|
62
|
+
### Error: "Z_ZERO_API_KEY is missing"
|
|
63
|
+
If your AI Agent reports this error, it means the `Z_ZERO_API_KEY` environment variable is not set correctly in your MCP configuration.
|
|
64
|
+
1. Go to [clawcard.store/dashboard/agents](https://clawcard.store/dashboard/agents).
|
|
65
|
+
2. Generate or Copy your **Passport Key** (starts with `zk_live_`).
|
|
66
|
+
3. Ensure it is correctly pasted into your `claude_desktop_config.json` or your IDE's MCP settings.
|
|
67
|
+
4. **Restart** your AI Agent (Claude Desktop or IDE) for the changes to take effect.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Troubleshooting
|
|
72
|
+
|
|
73
|
+
### Error: "Z_ZERO_API_KEY is missing"
|
|
74
|
+
This error means your AI Agent (Claude/Cursor) doesn't have your **Passport Key** yet.
|
|
75
|
+
1. Go to [clawcard.store/dashboard/agents](https://clawcard.store/dashboard/agents).
|
|
76
|
+
2. Copy your **Passport Key** (starts with `zk_live_`).
|
|
77
|
+
3. Add it to your config (e.g., `claude_desktop_config.json`) as `Z_ZERO_API_KEY`.
|
|
78
|
+
4. **Restart** your AI Agent (Claude Desktop or IDE).
|
|
79
|
+
|
|
80
|
+
*Security Note: OpenClaw never stores your Passport Key. It is passed via environment variables and card data exists only in RAM during execution.*
|
package/dist/api_backend.d.ts
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
import type { CardData } from "./types.js";
|
|
2
|
-
export declare function listCardsRemote(): Promise<
|
|
3
|
-
|
|
4
|
-
|
|
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>;
|
|
2
|
+
export declare function listCardsRemote(): Promise<any>;
|
|
3
|
+
export declare function getBalanceRemote(cardAlias: string): Promise<any>;
|
|
4
|
+
export declare function getDepositAddressesRemote(): Promise<any>;
|
|
15
5
|
export declare function issueTokenRemote(cardAlias: string, amount: number, merchant: string): Promise<any | null>;
|
|
16
6
|
export declare function resolveTokenRemote(token: string): Promise<CardData | null>;
|
|
17
7
|
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
|
-
}>;
|
|
8
|
+
export declare function cancelTokenRemote(token: string): Promise<any>;
|
|
22
9
|
export declare function refundUnderspendRemote(token: string, actualSpent: number): Promise<void>;
|
package/dist/api_backend.js
CHANGED
|
@@ -14,9 +14,14 @@ exports.refundUnderspendRemote = refundUnderspendRemote;
|
|
|
14
14
|
const API_BASE_URL = process.env.Z_ZERO_API_BASE_URL || "https://clawcard.store";
|
|
15
15
|
const PASSPORT_KEY = process.env.Z_ZERO_API_KEY || "";
|
|
16
16
|
if (!PASSPORT_KEY) {
|
|
17
|
-
console.error("❌
|
|
17
|
+
console.error("❌ ERROR: Z_ZERO_API_KEY (Passport Key) is missing!");
|
|
18
|
+
console.error("🔐 Please get your Passport Key from: https://clawcard.store/dashboard/agents");
|
|
19
|
+
console.error("🛠️ Setup: Ensure 'Z_ZERO_API_KEY' is set in your environment variables.");
|
|
18
20
|
}
|
|
19
21
|
async function apiRequest(endpoint, method = 'GET', body = null) {
|
|
22
|
+
if (!PASSPORT_KEY) {
|
|
23
|
+
return { error: "AUTH_REQUIRED", message: "Z_ZERO_API_KEY is missing. Human needs to set it in MCP config." };
|
|
24
|
+
}
|
|
20
25
|
const url = `${API_BASE_URL.replace(/\/$/, '')}${endpoint}`;
|
|
21
26
|
try {
|
|
22
27
|
const res = await fetch(url, {
|
|
@@ -30,29 +35,30 @@ async function apiRequest(endpoint, method = 'GET', body = null) {
|
|
|
30
35
|
if (!res.ok) {
|
|
31
36
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
32
37
|
console.error(`[API ERROR] ${endpoint}:`, err.error);
|
|
33
|
-
return
|
|
38
|
+
return { error: "API_ERROR", message: err.error || res.statusText };
|
|
34
39
|
}
|
|
35
40
|
return await res.json();
|
|
36
41
|
}
|
|
37
42
|
catch (err) {
|
|
38
43
|
console.error(`[NETWORK ERROR] ${endpoint}:`, err.message);
|
|
39
|
-
return
|
|
44
|
+
return { error: "NETWORK_ERROR", message: err.message };
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
async function listCardsRemote() {
|
|
43
|
-
|
|
44
|
-
return data?.cards || [];
|
|
48
|
+
return await apiRequest('/api/tokens/cards', 'GET');
|
|
45
49
|
}
|
|
46
50
|
async function getBalanceRemote(cardAlias) {
|
|
47
|
-
const
|
|
48
|
-
|
|
51
|
+
const data = await listCardsRemote();
|
|
52
|
+
if (data?.error)
|
|
53
|
+
return data;
|
|
54
|
+
const cards = data?.cards || [];
|
|
55
|
+
const card = cards.find((c) => c.alias === cardAlias);
|
|
49
56
|
if (!card)
|
|
50
57
|
return null;
|
|
51
58
|
return { balance: card.balance, currency: card.currency };
|
|
52
59
|
}
|
|
53
60
|
async function getDepositAddressesRemote() {
|
|
54
|
-
|
|
55
|
-
return data?.deposit_addresses || null;
|
|
61
|
+
return await apiRequest('/api/tokens/cards', 'GET');
|
|
56
62
|
}
|
|
57
63
|
async function issueTokenRemote(cardAlias, amount, merchant) {
|
|
58
64
|
const data = await apiRequest('/api/tokens/issue', 'POST', {
|
|
@@ -98,6 +104,8 @@ async function burnTokenRemote(token, receipt_id) {
|
|
|
98
104
|
}
|
|
99
105
|
async function cancelTokenRemote(token) {
|
|
100
106
|
const data = await apiRequest('/api/tokens/cancel', 'POST', { token });
|
|
107
|
+
if (data?.error)
|
|
108
|
+
return data;
|
|
101
109
|
return {
|
|
102
110
|
success: !!data,
|
|
103
111
|
refunded_amount: data?.refunded_amount || 0
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
//
|
|
3
|
+
// OpenClaw MCP Server (z-zero-mcp-server) v1.0.3
|
|
4
4
|
// Exposes secure JIT payment tools to AI Agents via Model Context Protocol
|
|
5
|
-
//
|
|
5
|
+
// Status: Connected to Z-ZERO Gateway — produces secure JIT virtual cards
|
|
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");
|
|
@@ -14,13 +14,25 @@ const playwright_bridge_js_1 = require("./playwright_bridge.js");
|
|
|
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.3",
|
|
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
|
|
23
|
+
const data = await (0, api_backend_js_1.listCardsRemote)();
|
|
24
|
+
if (data?.error === "AUTH_REQUIRED") {
|
|
25
|
+
return {
|
|
26
|
+
content: [{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
29
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents\n" +
|
|
30
|
+
"👉 Then SET it as the 'Z_ZERO_API_KEY' environment variable in your AI tool (Claude Desktop/Cursor) and RESTART the tool."
|
|
31
|
+
}],
|
|
32
|
+
isError: true
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const cards = data?.cards || [];
|
|
24
36
|
return {
|
|
25
37
|
content: [
|
|
26
38
|
{
|
|
@@ -41,13 +53,24 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
|
|
|
41
53
|
.string()
|
|
42
54
|
.describe("The alias of the card to check, e.g. 'Card_01'"),
|
|
43
55
|
}, async ({ card_alias }) => {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
56
|
+
const data = await (0, api_backend_js_1.getBalanceRemote)(card_alias);
|
|
57
|
+
if (data?.error === "AUTH_REQUIRED") {
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
62
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents\n" +
|
|
63
|
+
"👉 Then SET it as the 'Z_ZERO_API_KEY' environment variable and RESTART."
|
|
64
|
+
}],
|
|
65
|
+
isError: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!data || data.error) {
|
|
46
69
|
return {
|
|
47
70
|
content: [
|
|
48
71
|
{
|
|
49
72
|
type: "text",
|
|
50
|
-
text: `Card "${card_alias}" not found or API
|
|
73
|
+
text: `Card "${card_alias}" not found or API issue. Use list_cards to see available cards.`,
|
|
51
74
|
},
|
|
52
75
|
],
|
|
53
76
|
isError: true,
|
|
@@ -57,7 +80,7 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
|
|
|
57
80
|
content: [
|
|
58
81
|
{
|
|
59
82
|
type: "text",
|
|
60
|
-
text: JSON.stringify({ card_alias, ...
|
|
83
|
+
text: JSON.stringify({ card_alias, ...data }, null, 2),
|
|
61
84
|
},
|
|
62
85
|
],
|
|
63
86
|
};
|
|
@@ -66,13 +89,25 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
|
|
|
66
89
|
// TOOL 2.5: Get deposit addresses (Phase 14 feature)
|
|
67
90
|
// ============================================================
|
|
68
91
|
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
|
|
92
|
+
const data = await (0, api_backend_js_1.getDepositAddressesRemote)();
|
|
93
|
+
if (data?.error === "AUTH_REQUIRED") {
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
98
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents\n" +
|
|
99
|
+
"👉 Then SET it as the 'Z_ZERO_API_KEY' environment variable and RESTART."
|
|
100
|
+
}],
|
|
101
|
+
isError: true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const addresses = data?.deposit_addresses;
|
|
70
105
|
if (!addresses) {
|
|
71
106
|
return {
|
|
72
107
|
content: [
|
|
73
108
|
{
|
|
74
109
|
type: "text",
|
|
75
|
-
text: "Failed to retrieve deposit addresses. Please ensure your
|
|
110
|
+
text: "Failed to retrieve deposit addresses. Please ensure your Z_ZERO_API_KEY (Passport Key) is valid. You can find it at https://clawcard.store/dashboard/agents",
|
|
76
111
|
},
|
|
77
112
|
],
|
|
78
113
|
isError: true,
|
|
@@ -102,9 +137,9 @@ server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM
|
|
|
102
137
|
};
|
|
103
138
|
});
|
|
104
139
|
// ============================================================
|
|
105
|
-
// TOOL 3: Request a temporary payment token (issues
|
|
140
|
+
// TOOL 3: Request a temporary payment token (issues secure JIT card)
|
|
106
141
|
// ============================================================
|
|
107
|
-
server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A
|
|
142
|
+
server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A secure single-use virtual card is issued via the Z-ZERO network. The token is valid for 30 minutes. Min: $1, Max: $100. Use this token with execute_payment to complete a purchase.", {
|
|
108
143
|
card_alias: zod_1.z
|
|
109
144
|
.string()
|
|
110
145
|
.describe("Which card to charge, e.g. 'Card_01'"),
|
|
@@ -118,15 +153,26 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
118
153
|
.describe("Name or URL of the merchant/service being purchased"),
|
|
119
154
|
}, async ({ card_alias, amount, merchant }) => {
|
|
120
155
|
const token = await (0, api_backend_js_1.issueTokenRemote)(card_alias, amount, merchant);
|
|
121
|
-
if (
|
|
122
|
-
|
|
156
|
+
if (token?.error === "AUTH_REQUIRED") {
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
161
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents"
|
|
162
|
+
}],
|
|
163
|
+
isError: true
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (!token || token.error) {
|
|
167
|
+
const balanceData = await (0, api_backend_js_1.getBalanceRemote)(card_alias);
|
|
168
|
+
const balance = balanceData?.balance;
|
|
123
169
|
return {
|
|
124
170
|
content: [
|
|
125
171
|
{
|
|
126
172
|
type: "text",
|
|
127
|
-
text: balance
|
|
128
|
-
? `Insufficient balance. Card "${card_alias}" has $${balance
|
|
129
|
-
: `Card "${card_alias}" not found
|
|
173
|
+
text: balance !== undefined
|
|
174
|
+
? `Insufficient balance. Card "${card_alias}" has $${balance} but you requested $${amount}. Or amount is outside the $1-$100 limit.`
|
|
175
|
+
: `Card "${card_alias}" not found, API key is invalid, or amount limit exceeded.`,
|
|
130
176
|
},
|
|
131
177
|
],
|
|
132
178
|
isError: true,
|
|
@@ -142,7 +188,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
142
188
|
amount: token.amount,
|
|
143
189
|
merchant: token.merchant,
|
|
144
190
|
expires_at: expiresAt,
|
|
145
|
-
|
|
191
|
+
card_issued: true,
|
|
146
192
|
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.",
|
|
147
193
|
}, null, 2),
|
|
148
194
|
},
|
|
@@ -167,7 +213,17 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
167
213
|
}, async ({ token, checkout_url, actual_amount }) => {
|
|
168
214
|
// Step 1: Resolve token → card data (RAM only)
|
|
169
215
|
const cardData = await (0, api_backend_js_1.resolveTokenRemote)(token);
|
|
170
|
-
if (
|
|
216
|
+
if (cardData?.error === "AUTH_REQUIRED") {
|
|
217
|
+
return {
|
|
218
|
+
content: [{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
221
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents"
|
|
222
|
+
}],
|
|
223
|
+
isError: true
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (!cardData || cardData.error) {
|
|
171
227
|
return {
|
|
172
228
|
content: [
|
|
173
229
|
{
|
|
@@ -214,7 +270,17 @@ server.tool("cancel_payment_token", "Cancel a payment token that has not been us
|
|
|
214
270
|
.describe("Reason for cancellation, e.g. 'Price mismatch: checkout shows $20 but token is $15'"),
|
|
215
271
|
}, async ({ token, reason }) => {
|
|
216
272
|
const result = await (0, api_backend_js_1.cancelTokenRemote)(token);
|
|
217
|
-
if (
|
|
273
|
+
if (result?.error === "AUTH_REQUIRED") {
|
|
274
|
+
return {
|
|
275
|
+
content: [{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
278
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents"
|
|
279
|
+
}],
|
|
280
|
+
isError: true
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (!result || !result.success) {
|
|
218
284
|
return {
|
|
219
285
|
content: [
|
|
220
286
|
{
|
|
@@ -337,8 +403,8 @@ When asked to make a purchase, execute the following steps precisely in order:
|
|
|
337
403
|
async function main() {
|
|
338
404
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
339
405
|
await server.connect(transport);
|
|
340
|
-
console.error("🔐
|
|
341
|
-
console.error("
|
|
406
|
+
console.error("🔐 OpenClaw MCP Server v1.0.3 running...");
|
|
407
|
+
console.error("Status: Secure & Connected to Z-ZERO Gateway");
|
|
342
408
|
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval");
|
|
343
409
|
}
|
|
344
410
|
main().catch(console.error);
|
package/dist/types.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface VirtualCard {
|
|
|
7
7
|
name: string;
|
|
8
8
|
balance: number;
|
|
9
9
|
currency: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
message?: string;
|
|
10
12
|
}
|
|
11
13
|
export interface PaymentToken {
|
|
12
14
|
token: string;
|
|
@@ -16,6 +18,8 @@ export interface PaymentToken {
|
|
|
16
18
|
created_at: number;
|
|
17
19
|
ttl_seconds: number;
|
|
18
20
|
used: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
message?: string;
|
|
19
23
|
}
|
|
20
24
|
export interface CardData {
|
|
21
25
|
number: string;
|
|
@@ -23,6 +27,8 @@ export interface CardData {
|
|
|
23
27
|
exp_year: string;
|
|
24
28
|
cvv: string;
|
|
25
29
|
name: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
message?: string;
|
|
26
32
|
}
|
|
27
33
|
export interface PaymentResult {
|
|
28
34
|
success: boolean;
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z-zero-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
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": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"start": "node dist/index.js",
|
|
10
|
-
"dev": "npx tsx src/index.ts"
|
|
10
|
+
"dev": "npx tsx src/index.ts",
|
|
11
|
+
"postinstall": "echo 'IMPORTANT: Run \"npx playwright install chromium\" to enable automated checkouts.'"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
13
14
|
"mcp",
|
|
@@ -28,14 +29,13 @@
|
|
|
28
29
|
],
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
31
|
-
"@supabase/supabase-js": "^2.49.4",
|
|
32
32
|
"dotenv": "^16.5.0",
|
|
33
33
|
"playwright": "^1.52.0",
|
|
34
34
|
"zod": "^3.25.11"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"
|
|
37
|
+
"@types/node": "^22.15.21",
|
|
38
38
|
"tsx": "^4.19.4",
|
|
39
|
-
"
|
|
39
|
+
"typescript": "^5.8.3"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { CardData, PaymentToken } from "./types.js";
|
|
2
|
-
interface ExtendedToken extends PaymentToken {
|
|
3
|
-
airwallex_card_id?: string;
|
|
4
|
-
cancelled?: boolean;
|
|
5
|
-
}
|
|
6
|
-
export declare function listCardsRemote(): Promise<Array<{
|
|
7
|
-
alias: string;
|
|
8
|
-
balance: number;
|
|
9
|
-
currency: string;
|
|
10
|
-
}>>;
|
|
11
|
-
export declare function getBalanceRemote(cardAlias: string): Promise<{
|
|
12
|
-
balance: number;
|
|
13
|
-
currency: string;
|
|
14
|
-
} | null>;
|
|
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
|
-
}>;
|
|
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
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
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
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
-
};
|
|
8
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.listCardsRemote = listCardsRemote;
|
|
10
|
-
exports.getBalanceRemote = getBalanceRemote;
|
|
11
|
-
exports.getDepositAddressesRemote = getDepositAddressesRemote;
|
|
12
|
-
exports.issueTokenRemote = issueTokenRemote;
|
|
13
|
-
exports.resolveTokenRemote = resolveTokenRemote;
|
|
14
|
-
exports.cancelTokenRemote = cancelTokenRemote;
|
|
15
|
-
exports.burnTokenRemote = burnTokenRemote;
|
|
16
|
-
exports.refundUnderspendRemote = refundUnderspendRemote;
|
|
17
|
-
const supabase_js_1 = require("@supabase/supabase-js");
|
|
18
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
19
|
-
// ============================================================
|
|
20
|
-
// SUPABASE CLIENT
|
|
21
|
-
// ============================================================
|
|
22
|
-
const SUPABASE_URL = process.env.Z_ZERO_SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
23
|
-
const SUPABASE_ANON_KEY = process.env.Z_ZERO_SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
|
24
|
-
const API_KEY = process.env.Z_ZERO_API_KEY || "";
|
|
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
|
-
}
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
console.error("⚠️ Supabase backend is disabled (Missing Z_ZERO_SUPABASE_URL/ANON_KEY).");
|
|
36
|
-
}
|
|
37
|
-
// ============================================================
|
|
38
|
-
// AIRWALLEX CONFIG
|
|
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
|
-
}
|
|
75
|
-
const tokenStore = new Map();
|
|
76
|
-
// ============================================================
|
|
77
|
-
// RESOLVE API KEY → USER INFO
|
|
78
|
-
// ============================================================
|
|
79
|
-
async function resolveApiKey() {
|
|
80
|
-
if (!API_KEY)
|
|
81
|
-
return null;
|
|
82
|
-
const { data: card, error } = await supabase
|
|
83
|
-
.from("cards")
|
|
84
|
-
.select("id, alias, user_id, allocated_limit_usd, is_active")
|
|
85
|
-
.eq("card_number_encrypted", API_KEY)
|
|
86
|
-
.eq("is_active", true)
|
|
87
|
-
.single();
|
|
88
|
-
if (error || !card) {
|
|
89
|
-
console.error("API Key not found:", error?.message);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
const { data: wallet } = await supabase
|
|
93
|
-
.from("wallets")
|
|
94
|
-
.select("balance")
|
|
95
|
-
.eq("user_id", card.user_id)
|
|
96
|
-
.single();
|
|
97
|
-
return {
|
|
98
|
-
userId: card.user_id,
|
|
99
|
-
walletBalance: Number(wallet?.balance || 0),
|
|
100
|
-
cardId: card.id,
|
|
101
|
-
cardAlias: card.alias,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
// ============================================================
|
|
105
|
-
// PUBLIC API
|
|
106
|
-
// ============================================================
|
|
107
|
-
async function listCardsRemote() {
|
|
108
|
-
const info = await resolveApiKey();
|
|
109
|
-
if (!info)
|
|
110
|
-
return [];
|
|
111
|
-
return [{ alias: info.cardAlias, balance: info.walletBalance, currency: "USD" }];
|
|
112
|
-
}
|
|
113
|
-
async function getBalanceRemote(cardAlias) {
|
|
114
|
-
const info = await resolveApiKey();
|
|
115
|
-
if (!info || info.cardAlias !== cardAlias)
|
|
116
|
-
return null;
|
|
117
|
-
return { balance: info.walletBalance, currency: "USD" };
|
|
118
|
-
}
|
|
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
|
|
149
|
-
const info = await resolveApiKey();
|
|
150
|
-
if (!info)
|
|
151
|
-
return null;
|
|
152
|
-
if (info.cardAlias !== cardAlias)
|
|
153
|
-
return null;
|
|
154
|
-
if (info.walletBalance < amount) {
|
|
155
|
-
console.error(`[ISSUE] Insufficient balance: $${info.walletBalance} < $${amount}`);
|
|
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
|
-
}
|
|
206
|
-
const token = {
|
|
207
|
-
token: tokenId,
|
|
208
|
-
card_alias: cardAlias,
|
|
209
|
-
amount,
|
|
210
|
-
merchant,
|
|
211
|
-
created_at: Date.now(),
|
|
212
|
-
ttl_seconds: ttlSeconds,
|
|
213
|
-
used: false,
|
|
214
|
-
cancelled: false,
|
|
215
|
-
airwallex_card_id: airwallexCardId,
|
|
216
|
-
};
|
|
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}`);
|
|
225
|
-
return token;
|
|
226
|
-
}
|
|
227
|
-
async function resolveTokenRemote(tokenId) {
|
|
228
|
-
const token = tokenStore.get(tokenId);
|
|
229
|
-
if (!token || token.used || token.cancelled)
|
|
230
|
-
return null;
|
|
231
|
-
const age = (Date.now() - token.created_at) / 1000;
|
|
232
|
-
if (age > token.ttl_seconds) {
|
|
233
|
-
tokenStore.delete(tokenId);
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
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...)");
|
|
263
|
-
return {
|
|
264
|
-
number: "4242424242424242",
|
|
265
|
-
exp_month: "12",
|
|
266
|
-
exp_year: "2030",
|
|
267
|
-
cvv: "123",
|
|
268
|
-
name: "Z-ZERO Agent",
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
async function cancelTokenRemote(tokenId) {
|
|
272
|
-
const token = tokenStore.get(tokenId);
|
|
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
|
|
296
|
-
const info = await resolveApiKey();
|
|
297
|
-
if (info) {
|
|
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);
|
|
304
|
-
await supabase
|
|
305
|
-
.from("wallets")
|
|
306
|
-
.update({ balance: currentBalance + token.amount })
|
|
307
|
-
.eq("user_id", info.userId);
|
|
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) {
|
|
321
|
-
await supabase.from("transactions").insert({
|
|
322
|
-
card_id: info.cardId,
|
|
323
|
-
amount: token.amount,
|
|
324
|
-
merchant: token.merchant,
|
|
325
|
-
status: "SUCCESS",
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
setTimeout(() => tokenStore.delete(tokenId), 5000);
|
|
329
|
-
return true;
|
|
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
|
-
}
|