z-zero-mcp-server 1.0.3 → 1.0.5
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 +86 -34
- package/dist/api_backend.d.ts +4 -17
- package/dist/api_backend.js +18 -10
- package/dist/index.js +97 -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,105 @@
|
|
|
1
|
-
# Z-ZERO
|
|
1
|
+
# OpenClaw: Z-ZERO AI Agent MCP Server
|
|
2
2
|
|
|
3
|
-
A Zero-Trust Payment Protocol built specifically for AI Agents
|
|
3
|
+
A Zero-Trust Payment Protocol built specifically for AI Agents using the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). Give your local agents (Claude, Cursor, AntiGravity) the ability to make real-world purchases — securely, without ever seeing a real card number.
|
|
4
4
|
|
|
5
|
-
##
|
|
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**.
|
|
5
|
+
## How It Works
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
Instead of giving your AI direct access to a credit card number, OpenClaw issues **temporary, single-use JIT tokens**. The token is resolved in RAM, Playwright injects the card data directly into the payment form, and the virtual card is burned milliseconds later. Your AI never sees the PAN, CVV, or expiry.
|
|
9
8
|
|
|
10
|
-
|
|
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.
|
|
9
|
+
---
|
|
15
10
|
|
|
16
|
-
##
|
|
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)
|
|
11
|
+
## Quick Install (Recommended)
|
|
22
12
|
|
|
23
|
-
## Getting Started
|
|
24
|
-
|
|
25
|
-
### 1. Install Dependencies
|
|
26
13
|
```bash
|
|
27
|
-
|
|
28
|
-
npx playwright install chromium
|
|
14
|
+
npx z-zero-mcp-server
|
|
29
15
|
```
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm run build
|
|
34
|
-
```
|
|
17
|
+
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
35
18
|
|
|
36
|
-
### 3. Usage with Claude Desktop
|
|
37
|
-
Add this to your `mcp_config.json`:
|
|
38
19
|
```json
|
|
39
20
|
{
|
|
40
21
|
"mcpServers": {
|
|
41
|
-
"
|
|
42
|
-
"command": "
|
|
43
|
-
"args": ["
|
|
22
|
+
"openclaw": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "z-zero-mcp-server"],
|
|
25
|
+
"env": {
|
|
26
|
+
"Z_ZERO_API_KEY": "zk_live_your_passport_key_here"
|
|
27
|
+
}
|
|
44
28
|
}
|
|
45
29
|
}
|
|
46
30
|
}
|
|
47
31
|
```
|
|
48
32
|
|
|
33
|
+
Get your Passport Key at: **[clawcard.store/dashboard/agents](https://www.clawcard.store/dashboard/agents)**
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- **Node.js v18+** — [nodejs.org](https://nodejs.org)
|
|
40
|
+
- **Passport Key** — starts with `zk_live_`, get it from the dashboard above
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
49
44
|
## Available MCP Tools
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
|
|
46
|
+
| Tool | Description |
|
|
47
|
+
|------|-------------|
|
|
48
|
+
| `list_cards` | View your virtual card aliases and balances |
|
|
49
|
+
| `check_balance` | Query a specific card's real-time balance |
|
|
50
|
+
| `get_deposit_addresses` | Get deposit addresses to top up your balance |
|
|
51
|
+
| `request_payment_token` | Generate a JIT auth token for a specific amount |
|
|
52
|
+
| `execute_payment` | Auto-fill checkout form and execute payment |
|
|
53
|
+
| `cancel_payment_token` | Cancel unused token, refund to wallet |
|
|
54
|
+
| `request_human_approval` | Pause and ask human for approval |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## REST API Reference
|
|
59
|
+
|
|
60
|
+
The Z-ZERO backend is hosted at `https://www.clawcard.store`. All endpoints require a `Bearer` token using your Passport Key.
|
|
61
|
+
|
|
62
|
+
> ⚠️ **Use the MCP tools above instead of calling REST directly.** If you must call REST, use the exact paths below.
|
|
63
|
+
|
|
64
|
+
### `GET /api/tokens/cards`
|
|
65
|
+
Returns your card list, balance, and deposit addresses.
|
|
66
|
+
```bash
|
|
67
|
+
curl -X GET "https://www.clawcard.store/api/tokens/cards" \
|
|
68
|
+
-H "Authorization: Bearer zk_live_your_key"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Aliases (also work):**
|
|
72
|
+
- `GET /api/v1/cards` ← for agents that guess REST-style paths
|
|
73
|
+
|
|
74
|
+
### `POST /api/tokens/issue`
|
|
75
|
+
Issue a JIT payment token.
|
|
76
|
+
|
|
77
|
+
### `POST /api/tokens/resolve`
|
|
78
|
+
Resolve a token to card data (server-side only).
|
|
79
|
+
|
|
80
|
+
### `POST /api/tokens/burn`
|
|
81
|
+
Burn a used token.
|
|
82
|
+
|
|
83
|
+
### `POST /api/tokens/cancel`
|
|
84
|
+
Cancel an unused token (refunds balance).
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Troubleshooting
|
|
89
|
+
|
|
90
|
+
### "Z_ZERO_API_KEY is missing"
|
|
91
|
+
1. Go to [clawcard.store/dashboard/agents](https://www.clawcard.store/dashboard/agents)
|
|
92
|
+
2. Copy your Passport Key (starts with `zk_live_`)
|
|
93
|
+
3. Add it to your config as `Z_ZERO_API_KEY`
|
|
94
|
+
4. **Restart** Claude Desktop / Cursor
|
|
95
|
+
|
|
96
|
+
### "Invalid API Key" (401)
|
|
97
|
+
- Double-check you copied the full key (e.g. `zk_live_c0g3l`)
|
|
98
|
+
- Make sure there are no extra spaces or line breaks
|
|
99
|
+
|
|
100
|
+
### "404 Not Found" on `/api/v1/cards`
|
|
101
|
+
- This is a legacy path alias — it should now work. If not, use `/api/tokens/cards` directly.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
*Security: OpenClaw never stores your Passport Key. It is passed via environment variables and card data exists only in volatile 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
|
@@ -11,12 +11,17 @@ exports.resolveTokenRemote = resolveTokenRemote;
|
|
|
11
11
|
exports.burnTokenRemote = burnTokenRemote;
|
|
12
12
|
exports.cancelTokenRemote = cancelTokenRemote;
|
|
13
13
|
exports.refundUnderspendRemote = refundUnderspendRemote;
|
|
14
|
-
const API_BASE_URL = process.env.Z_ZERO_API_BASE_URL || "https://clawcard.store";
|
|
14
|
+
const API_BASE_URL = process.env.Z_ZERO_API_BASE_URL || "https://www.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://www.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,34 @@ 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://www.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
|
+
if (data?.error) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `❌ API ERROR: ${data.message || data.error}\n\nCould not fetch cards. Please verify your Passport Key is correct.`
|
|
40
|
+
}],
|
|
41
|
+
isError: true
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const cards = data?.cards || [];
|
|
24
45
|
return {
|
|
25
46
|
content: [
|
|
26
47
|
{
|
|
@@ -41,13 +62,24 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
|
|
|
41
62
|
.string()
|
|
42
63
|
.describe("The alias of the card to check, e.g. 'Card_01'"),
|
|
43
64
|
}, async ({ card_alias }) => {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
65
|
+
const data = await (0, api_backend_js_1.getBalanceRemote)(card_alias);
|
|
66
|
+
if (data?.error === "AUTH_REQUIRED") {
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
71
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents\n" +
|
|
72
|
+
"👉 Then SET it as the 'Z_ZERO_API_KEY' environment variable and RESTART."
|
|
73
|
+
}],
|
|
74
|
+
isError: true
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (!data || data.error) {
|
|
46
78
|
return {
|
|
47
79
|
content: [
|
|
48
80
|
{
|
|
49
81
|
type: "text",
|
|
50
|
-
text: `Card "${card_alias}" not found or API
|
|
82
|
+
text: `Card "${card_alias}" not found or API issue. Use list_cards to see available cards.`,
|
|
51
83
|
},
|
|
52
84
|
],
|
|
53
85
|
isError: true,
|
|
@@ -57,7 +89,7 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
|
|
|
57
89
|
content: [
|
|
58
90
|
{
|
|
59
91
|
type: "text",
|
|
60
|
-
text: JSON.stringify({ card_alias, ...
|
|
92
|
+
text: JSON.stringify({ card_alias, ...data }, null, 2),
|
|
61
93
|
},
|
|
62
94
|
],
|
|
63
95
|
};
|
|
@@ -66,13 +98,25 @@ server.tool("check_balance", "Check the remaining balance of a virtual card by i
|
|
|
66
98
|
// TOOL 2.5: Get deposit addresses (Phase 14 feature)
|
|
67
99
|
// ============================================================
|
|
68
100
|
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
|
|
101
|
+
const data = await (0, api_backend_js_1.getDepositAddressesRemote)();
|
|
102
|
+
if (data?.error === "AUTH_REQUIRED") {
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
107
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents\n" +
|
|
108
|
+
"👉 Then SET it as the 'Z_ZERO_API_KEY' environment variable and RESTART."
|
|
109
|
+
}],
|
|
110
|
+
isError: true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const addresses = data?.deposit_addresses;
|
|
70
114
|
if (!addresses) {
|
|
71
115
|
return {
|
|
72
116
|
content: [
|
|
73
117
|
{
|
|
74
118
|
type: "text",
|
|
75
|
-
text: "Failed to retrieve deposit addresses. Please ensure your
|
|
119
|
+
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
120
|
},
|
|
77
121
|
],
|
|
78
122
|
isError: true,
|
|
@@ -102,9 +146,9 @@ server.tool("get_deposit_addresses", "Get your unique deposit addresses for EVM
|
|
|
102
146
|
};
|
|
103
147
|
});
|
|
104
148
|
// ============================================================
|
|
105
|
-
// TOOL 3: Request a temporary payment token (issues
|
|
149
|
+
// TOOL 3: Request a temporary payment token (issues secure JIT card)
|
|
106
150
|
// ============================================================
|
|
107
|
-
server.tool("request_payment_token", "Request a temporary payment token for a specific amount. A
|
|
151
|
+
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
152
|
card_alias: zod_1.z
|
|
109
153
|
.string()
|
|
110
154
|
.describe("Which card to charge, e.g. 'Card_01'"),
|
|
@@ -118,15 +162,26 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
118
162
|
.describe("Name or URL of the merchant/service being purchased"),
|
|
119
163
|
}, async ({ card_alias, amount, merchant }) => {
|
|
120
164
|
const token = await (0, api_backend_js_1.issueTokenRemote)(card_alias, amount, merchant);
|
|
121
|
-
if (
|
|
122
|
-
|
|
165
|
+
if (token?.error === "AUTH_REQUIRED") {
|
|
166
|
+
return {
|
|
167
|
+
content: [{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
170
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents"
|
|
171
|
+
}],
|
|
172
|
+
isError: true
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (!token || token.error) {
|
|
176
|
+
const balanceData = await (0, api_backend_js_1.getBalanceRemote)(card_alias);
|
|
177
|
+
const balance = balanceData?.balance;
|
|
123
178
|
return {
|
|
124
179
|
content: [
|
|
125
180
|
{
|
|
126
181
|
type: "text",
|
|
127
|
-
text: balance
|
|
128
|
-
? `Insufficient balance. Card "${card_alias}" has $${balance
|
|
129
|
-
: `Card "${card_alias}" not found
|
|
182
|
+
text: balance !== undefined
|
|
183
|
+
? `Insufficient balance. Card "${card_alias}" has $${balance} but you requested $${amount}. Or amount is outside the $1-$100 limit.`
|
|
184
|
+
: `Card "${card_alias}" not found, API key is invalid, or amount limit exceeded.`,
|
|
130
185
|
},
|
|
131
186
|
],
|
|
132
187
|
isError: true,
|
|
@@ -142,7 +197,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
142
197
|
amount: token.amount,
|
|
143
198
|
merchant: token.merchant,
|
|
144
199
|
expires_at: expiresAt,
|
|
145
|
-
|
|
200
|
+
card_issued: true,
|
|
146
201
|
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
202
|
}, null, 2),
|
|
148
203
|
},
|
|
@@ -167,7 +222,17 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
167
222
|
}, async ({ token, checkout_url, actual_amount }) => {
|
|
168
223
|
// Step 1: Resolve token → card data (RAM only)
|
|
169
224
|
const cardData = await (0, api_backend_js_1.resolveTokenRemote)(token);
|
|
170
|
-
if (
|
|
225
|
+
if (cardData?.error === "AUTH_REQUIRED") {
|
|
226
|
+
return {
|
|
227
|
+
content: [{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
230
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents"
|
|
231
|
+
}],
|
|
232
|
+
isError: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (!cardData || cardData.error) {
|
|
171
236
|
return {
|
|
172
237
|
content: [
|
|
173
238
|
{
|
|
@@ -214,7 +279,17 @@ server.tool("cancel_payment_token", "Cancel a payment token that has not been us
|
|
|
214
279
|
.describe("Reason for cancellation, e.g. 'Price mismatch: checkout shows $20 but token is $15'"),
|
|
215
280
|
}, async ({ token, reason }) => {
|
|
216
281
|
const result = await (0, api_backend_js_1.cancelTokenRemote)(token);
|
|
217
|
-
if (
|
|
282
|
+
if (result?.error === "AUTH_REQUIRED") {
|
|
283
|
+
return {
|
|
284
|
+
content: [{
|
|
285
|
+
type: "text",
|
|
286
|
+
text: "❌ AUTHENTICATION REQUIRED: Your Z_ZERO_API_KEY (Passport Key) is missing from the MCP configuration.\n\n" +
|
|
287
|
+
"👉 Please GET your key here: https://clawcard.store/dashboard/agents"
|
|
288
|
+
}],
|
|
289
|
+
isError: true
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (!result || !result.success) {
|
|
218
293
|
return {
|
|
219
294
|
content: [
|
|
220
295
|
{
|
|
@@ -337,8 +412,8 @@ When asked to make a purchase, execute the following steps precisely in order:
|
|
|
337
412
|
async function main() {
|
|
338
413
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
339
414
|
await server.connect(transport);
|
|
340
|
-
console.error("🔐
|
|
341
|
-
console.error("
|
|
415
|
+
console.error("🔐 OpenClaw MCP Server v1.0.3 running...");
|
|
416
|
+
console.error("Status: Secure & Connected to Z-ZERO Gateway");
|
|
342
417
|
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval");
|
|
343
418
|
}
|
|
344
419
|
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.5",
|
|
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
|
-
}
|