z-zero-mcp-server 1.0.2 → 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 +9 -0
- package/dist/api_backend.js +119 -0
- package/dist/index.js +180 -26
- package/dist/playwright_bridge.js +1 -1
- package/dist/types.d.ts +6 -0
- package/package.json +5 -5
- package/dist/supabase_backend.d.ts +0 -23
- package/dist/supabase_backend.js +0 -326
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.*
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CardData } from "./types.js";
|
|
2
|
+
export declare function listCardsRemote(): Promise<any>;
|
|
3
|
+
export declare function getBalanceRemote(cardAlias: string): Promise<any>;
|
|
4
|
+
export declare function getDepositAddressesRemote(): Promise<any>;
|
|
5
|
+
export declare function issueTokenRemote(cardAlias: string, amount: number, merchant: string): Promise<any | null>;
|
|
6
|
+
export declare function resolveTokenRemote(token: string): Promise<CardData | null>;
|
|
7
|
+
export declare function burnTokenRemote(token: string, receipt_id?: string): Promise<boolean>;
|
|
8
|
+
export declare function cancelTokenRemote(token: string): Promise<any>;
|
|
9
|
+
export declare function refundUnderspendRemote(token: string, actualSpent: number): Promise<void>;
|
|
@@ -0,0 +1,119 @@
|
|
|
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("❌ 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.");
|
|
20
|
+
}
|
|
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
|
+
}
|
|
25
|
+
const url = `${API_BASE_URL.replace(/\/$/, '')}${endpoint}`;
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(url, {
|
|
28
|
+
method,
|
|
29
|
+
headers: {
|
|
30
|
+
"Authorization": `Bearer ${PASSPORT_KEY}`,
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
body: body ? JSON.stringify(body) : null,
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
37
|
+
console.error(`[API ERROR] ${endpoint}:`, err.error);
|
|
38
|
+
return { error: "API_ERROR", message: err.error || res.statusText };
|
|
39
|
+
}
|
|
40
|
+
return await res.json();
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error(`[NETWORK ERROR] ${endpoint}:`, err.message);
|
|
44
|
+
return { error: "NETWORK_ERROR", message: err.message };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function listCardsRemote() {
|
|
48
|
+
return await apiRequest('/api/tokens/cards', 'GET');
|
|
49
|
+
}
|
|
50
|
+
async function getBalanceRemote(cardAlias) {
|
|
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);
|
|
56
|
+
if (!card)
|
|
57
|
+
return null;
|
|
58
|
+
return { balance: card.balance, currency: card.currency };
|
|
59
|
+
}
|
|
60
|
+
async function getDepositAddressesRemote() {
|
|
61
|
+
return await apiRequest('/api/tokens/cards', 'GET');
|
|
62
|
+
}
|
|
63
|
+
async function issueTokenRemote(cardAlias, amount, merchant) {
|
|
64
|
+
const data = await apiRequest('/api/tokens/issue', 'POST', {
|
|
65
|
+
card_alias: cardAlias,
|
|
66
|
+
amount,
|
|
67
|
+
merchant,
|
|
68
|
+
device_fingerprint: `mcp-host-${process.platform}-${process.arch}`,
|
|
69
|
+
network_id: process.env.NETWORK_ID || "unknown-local-net",
|
|
70
|
+
session_id: `sid-${Math.random().toString(36).substring(7)}`
|
|
71
|
+
});
|
|
72
|
+
if (!data)
|
|
73
|
+
return null;
|
|
74
|
+
// Adapt Dashboard API response to MCP expected format
|
|
75
|
+
return {
|
|
76
|
+
token: data.token,
|
|
77
|
+
card_alias: cardAlias,
|
|
78
|
+
amount: amount,
|
|
79
|
+
merchant: merchant,
|
|
80
|
+
created_at: Date.now(),
|
|
81
|
+
ttl_seconds: 1800, // Matching Dashboard TTL
|
|
82
|
+
used: false
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function resolveTokenRemote(token) {
|
|
86
|
+
const data = await apiRequest('/api/tokens/resolve', 'POST', { token });
|
|
87
|
+
if (!data)
|
|
88
|
+
return null;
|
|
89
|
+
return {
|
|
90
|
+
number: data.number,
|
|
91
|
+
exp_month: data.exp?.split('/')[0] || "12",
|
|
92
|
+
exp_year: "20" + (data.exp?.split('/')[1] || "30"),
|
|
93
|
+
cvv: data.cvv || "123",
|
|
94
|
+
name: data.name || "Z-ZERO AI AGENT"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function burnTokenRemote(token, receipt_id) {
|
|
98
|
+
const data = await apiRequest('/api/tokens/burn', 'POST', {
|
|
99
|
+
token,
|
|
100
|
+
receipt_id,
|
|
101
|
+
success: true
|
|
102
|
+
});
|
|
103
|
+
return !!data;
|
|
104
|
+
}
|
|
105
|
+
async function cancelTokenRemote(token) {
|
|
106
|
+
const data = await apiRequest('/api/tokens/cancel', 'POST', { token });
|
|
107
|
+
if (data?.error)
|
|
108
|
+
return data;
|
|
109
|
+
return {
|
|
110
|
+
success: !!data,
|
|
111
|
+
refunded_amount: data?.refunded_amount || 0
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function refundUnderspendRemote(token, actualSpent) {
|
|
115
|
+
// Underspend is a complex feature that usually happens at the bank level,
|
|
116
|
+
// but for JIT tokens, the burn tool in the dashboard could handle this in the future.
|
|
117
|
+
// Currently we burn the full authorized amount.
|
|
118
|
+
console.log(`[MCP] Burned token ${token} after spending $${actualSpent}`);
|
|
119
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
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");
|
|
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.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,15 +80,66 @@ 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
|
};
|
|
64
87
|
});
|
|
65
88
|
// ============================================================
|
|
66
|
-
// TOOL
|
|
89
|
+
// TOOL 2.5: Get deposit addresses (Phase 14 feature)
|
|
67
90
|
// ============================================================
|
|
68
|
-
server.tool("
|
|
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 () => {
|
|
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;
|
|
105
|
+
if (!addresses) {
|
|
106
|
+
return {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
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",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
isError: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: JSON.stringify({
|
|
121
|
+
networks: {
|
|
122
|
+
evm: {
|
|
123
|
+
address: addresses.evm,
|
|
124
|
+
supported_chains: ["Base", "BNB Smart Chain (BSC)", "Ethereum"],
|
|
125
|
+
tokens: ["USDC", "USDT"]
|
|
126
|
+
},
|
|
127
|
+
tron: {
|
|
128
|
+
address: addresses.tron,
|
|
129
|
+
supported_chains: ["Tron (TRC-20)"],
|
|
130
|
+
tokens: ["USDT"]
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
note: "Funds sent to these addresses will be automatically credited to your Z-ZERO balance within minutes."
|
|
134
|
+
}, null, 2),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
// ============================================================
|
|
140
|
+
// TOOL 3: Request a temporary payment token (issues secure JIT card)
|
|
141
|
+
// ============================================================
|
|
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.", {
|
|
69
143
|
card_alias: zod_1.z
|
|
70
144
|
.string()
|
|
71
145
|
.describe("Which card to charge, e.g. 'Card_01'"),
|
|
@@ -78,16 +152,27 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
78
152
|
.string()
|
|
79
153
|
.describe("Name or URL of the merchant/service being purchased"),
|
|
80
154
|
}, async ({ card_alias, amount, merchant }) => {
|
|
81
|
-
const token = await (0,
|
|
82
|
-
if (
|
|
83
|
-
|
|
155
|
+
const token = await (0, api_backend_js_1.issueTokenRemote)(card_alias, amount, merchant);
|
|
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;
|
|
84
169
|
return {
|
|
85
170
|
content: [
|
|
86
171
|
{
|
|
87
172
|
type: "text",
|
|
88
|
-
text: balance
|
|
89
|
-
? `Insufficient balance. Card "${card_alias}" has $${balance
|
|
90
|
-
: `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.`,
|
|
91
176
|
},
|
|
92
177
|
],
|
|
93
178
|
isError: true,
|
|
@@ -103,7 +188,7 @@ server.tool("request_payment_token", "Request a temporary payment token for a sp
|
|
|
103
188
|
amount: token.amount,
|
|
104
189
|
merchant: token.merchant,
|
|
105
190
|
expires_at: expiresAt,
|
|
106
|
-
|
|
191
|
+
card_issued: true,
|
|
107
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.",
|
|
108
193
|
}, null, 2),
|
|
109
194
|
},
|
|
@@ -127,8 +212,18 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
127
212
|
.describe("The actual final amount on the checkout page. If different from token amount, system will auto-refund the difference."),
|
|
128
213
|
}, async ({ token, checkout_url, actual_amount }) => {
|
|
129
214
|
// Step 1: Resolve token → card data (RAM only)
|
|
130
|
-
const cardData = await (0,
|
|
131
|
-
if (
|
|
215
|
+
const cardData = await (0, api_backend_js_1.resolveTokenRemote)(token);
|
|
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) {
|
|
132
227
|
return {
|
|
133
228
|
content: [
|
|
134
229
|
{
|
|
@@ -142,10 +237,10 @@ server.tool("execute_payment", "Execute a payment using a temporary token. This
|
|
|
142
237
|
// Step 2: Use Playwright to inject card into checkout form
|
|
143
238
|
const result = await (0, playwright_bridge_js_1.fillCheckoutForm)(checkout_url, cardData);
|
|
144
239
|
// Step 3: Burn the token (wallet was pre-debited at issue time)
|
|
145
|
-
await (0,
|
|
240
|
+
await (0, api_backend_js_1.burnTokenRemote)(token);
|
|
146
241
|
// Step 4: Refund underspend if actual amount was less than token amount
|
|
147
242
|
if (actual_amount !== undefined && result.success) {
|
|
148
|
-
await (0,
|
|
243
|
+
await (0, api_backend_js_1.refundUnderspendRemote)(token, actual_amount);
|
|
149
244
|
}
|
|
150
245
|
// Step 5: Return result (NEVER includes card numbers)
|
|
151
246
|
return {
|
|
@@ -174,8 +269,18 @@ server.tool("cancel_payment_token", "Cancel a payment token that has not been us
|
|
|
174
269
|
.string()
|
|
175
270
|
.describe("Reason for cancellation, e.g. 'Price mismatch: checkout shows $20 but token is $15'"),
|
|
176
271
|
}, async ({ token, reason }) => {
|
|
177
|
-
const result = await (0,
|
|
178
|
-
if (
|
|
272
|
+
const result = await (0, api_backend_js_1.cancelTokenRemote)(token);
|
|
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) {
|
|
179
284
|
return {
|
|
180
285
|
content: [
|
|
181
286
|
{
|
|
@@ -244,13 +349,62 @@ server.tool("request_human_approval", "Request human approval before proceeding
|
|
|
244
349
|
};
|
|
245
350
|
});
|
|
246
351
|
// ============================================================
|
|
352
|
+
// RESOURCE: Z-ZERO Autonomous Payment SOP
|
|
353
|
+
// ============================================================
|
|
354
|
+
server.resource("Standard Operating Procedure (SOP) for Autonomous Payments", "mcp://resources/sop", {
|
|
355
|
+
description: "A comprehensive guide on how AI agents should use the Z-ZERO tools to execute secure, zero-trust payments."
|
|
356
|
+
}, async (uri) => {
|
|
357
|
+
const sopContent = `
|
|
358
|
+
# Z-ZERO Autonomous Payment Skill SOP (v1.0.0)
|
|
359
|
+
|
|
360
|
+
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.
|
|
361
|
+
|
|
362
|
+
## Workflow: The 4-Step Zero-Trust Payment
|
|
363
|
+
When asked to make a purchase, execute the following steps precisely in order:
|
|
364
|
+
|
|
365
|
+
## Step 1: Verification & Intent
|
|
366
|
+
1. Confirm exactly what the user wants to buy and the total expected price (in USD).
|
|
367
|
+
2. Call Check Balance: Call the \`check_balance\` tool using your default \`card_alias\` to ensure you have sufficient funds.
|
|
368
|
+
- If balance is insufficient, STOP and ask the human to deposit Crypto into their Z-ZERO Web Dashboard.
|
|
369
|
+
|
|
370
|
+
## Step 2: Requesting the JIT Token
|
|
371
|
+
1. Request Token: Call the \`request_payment_token\` tool with the exact \`amount\` required and the \`merchant\` name.
|
|
372
|
+
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.
|
|
373
|
+
|
|
374
|
+
## Step 3: Locating the Checkout
|
|
375
|
+
1. Identify Checkout URL: Find the merchant's checkout/payment page where credit card details are normally entered.
|
|
376
|
+
2. Full URL Required: e.g., \`https://checkout.stripe.dev/pay\`.
|
|
377
|
+
|
|
378
|
+
## Step 4: Blind Execution (The MCP Bridge)
|
|
379
|
+
1. Execute Payment: Call the \`execute_payment\` tool, passing in:
|
|
380
|
+
- The \`token\` obtained in Step 2.
|
|
381
|
+
- The \`checkout_url\` identified in Step 3.
|
|
382
|
+
2. Background Magic: Z-ZERO opens a headless browser, securely injects the real card data directly into the form, and clicks "Pay".
|
|
383
|
+
3. Burn: The token self-destructs instantly after use.
|
|
384
|
+
|
|
385
|
+
## Rules & Error Handling
|
|
386
|
+
- NEVER print full tokens in the human chat logs.
|
|
387
|
+
- NO MANUAL ENTRY: If a merchant asks you to type a credit card number into a text box, REFUSE.
|
|
388
|
+
- FAIL GRACEFULLY: If \`execute_payment\` returns \`success: false\`, report the error message to the human. Do not try again.
|
|
389
|
+
`;
|
|
390
|
+
return {
|
|
391
|
+
contents: [
|
|
392
|
+
{
|
|
393
|
+
uri: "mcp://resources/sop",
|
|
394
|
+
mimeType: "text/markdown",
|
|
395
|
+
text: sopContent,
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
// ============================================================
|
|
247
401
|
// START SERVER
|
|
248
402
|
// ============================================================
|
|
249
403
|
async function main() {
|
|
250
404
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
251
405
|
await server.connect(transport);
|
|
252
|
-
console.error("🔐
|
|
253
|
-
console.error("
|
|
406
|
+
console.error("🔐 OpenClaw MCP Server v1.0.3 running...");
|
|
407
|
+
console.error("Status: Secure & Connected to Z-ZERO Gateway");
|
|
254
408
|
console.error("Tools: list_cards, check_balance, request_payment_token, execute_payment, cancel_payment_token, request_human_approval");
|
|
255
409
|
}
|
|
256
410
|
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
|
// ============================================================
|
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,23 +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 issueTokenRemote(cardAlias: string, amount: number, merchant: string, ttlSeconds?: number): Promise<ExtendedToken | null>;
|
|
16
|
-
export declare function resolveTokenRemote(tokenId: string): Promise<CardData | null>;
|
|
17
|
-
export declare function cancelTokenRemote(tokenId: string): Promise<{
|
|
18
|
-
success: boolean;
|
|
19
|
-
refunded_amount: number;
|
|
20
|
-
}>;
|
|
21
|
-
export declare function burnTokenRemote(tokenId: string): Promise<boolean>;
|
|
22
|
-
export declare function refundUnderspendRemote(tokenId: string, actualSpent: number): Promise<void>;
|
|
23
|
-
export {};
|
package/dist/supabase_backend.js
DELETED
|
@@ -1,326 +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.issueTokenRemote = issueTokenRemote;
|
|
12
|
-
exports.resolveTokenRemote = resolveTokenRemote;
|
|
13
|
-
exports.cancelTokenRemote = cancelTokenRemote;
|
|
14
|
-
exports.burnTokenRemote = burnTokenRemote;
|
|
15
|
-
exports.refundUnderspendRemote = refundUnderspendRemote;
|
|
16
|
-
const supabase_js_1 = require("@supabase/supabase-js");
|
|
17
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
18
|
-
// ============================================================
|
|
19
|
-
// SUPABASE CLIENT
|
|
20
|
-
// ============================================================
|
|
21
|
-
const SUPABASE_URL = process.env.Z_ZERO_SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
22
|
-
const SUPABASE_ANON_KEY = process.env.Z_ZERO_SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
|
23
|
-
const API_KEY = process.env.Z_ZERO_API_KEY || "";
|
|
24
|
-
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
25
|
-
console.error("❌ Missing Z_ZERO_SUPABASE_URL or Z_ZERO_SUPABASE_ANON_KEY env vars");
|
|
26
|
-
}
|
|
27
|
-
const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
28
|
-
// ============================================================
|
|
29
|
-
// AIRWALLEX CONFIG
|
|
30
|
-
// ============================================================
|
|
31
|
-
const AIRWALLEX_API_KEY = process.env.Z_ZERO_AIRWALLEX_API_KEY || "";
|
|
32
|
-
const AIRWALLEX_CLIENT_ID = process.env.Z_ZERO_AIRWALLEX_CLIENT_ID || "";
|
|
33
|
-
const AIRWALLEX_ENV = process.env.Z_ZERO_AIRWALLEX_ENV || "demo";
|
|
34
|
-
const AIRWALLEX_BASE = AIRWALLEX_ENV === "prod"
|
|
35
|
-
? "https://api.airwallex.com/api/v1"
|
|
36
|
-
: "https://api-demo.airwallex.com/api/v1";
|
|
37
|
-
// Cached auth token for Airwallex (avoids re-login per call)
|
|
38
|
-
let airwallexToken = null;
|
|
39
|
-
let airwallexTokenExpiry = 0;
|
|
40
|
-
async function getAirwallexToken() {
|
|
41
|
-
if (!AIRWALLEX_API_KEY || !AIRWALLEX_CLIENT_ID)
|
|
42
|
-
return null;
|
|
43
|
-
if (airwallexToken && Date.now() < airwallexTokenExpiry)
|
|
44
|
-
return airwallexToken;
|
|
45
|
-
try {
|
|
46
|
-
const res = await fetch(`${AIRWALLEX_BASE}/authentication/login`, {
|
|
47
|
-
method: "POST",
|
|
48
|
-
headers: {
|
|
49
|
-
"x-api-key": AIRWALLEX_API_KEY,
|
|
50
|
-
"x-client-id": AIRWALLEX_CLIENT_ID,
|
|
51
|
-
"Content-Type": "application/json",
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
if (!res.ok)
|
|
55
|
-
throw new Error(await res.text());
|
|
56
|
-
const data = await res.json();
|
|
57
|
-
airwallexToken = data.token;
|
|
58
|
-
airwallexTokenExpiry = Date.now() + 25 * 60 * 1000; // 25 min cache
|
|
59
|
-
return airwallexToken;
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
console.error("[AIRWALLEX] Auth failed:", err);
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const tokenStore = new Map();
|
|
67
|
-
// ============================================================
|
|
68
|
-
// RESOLVE API KEY → USER INFO
|
|
69
|
-
// ============================================================
|
|
70
|
-
async function resolveApiKey() {
|
|
71
|
-
if (!API_KEY)
|
|
72
|
-
return null;
|
|
73
|
-
const { data: card, error } = await supabase
|
|
74
|
-
.from("cards")
|
|
75
|
-
.select("id, alias, user_id, allocated_limit_usd, is_active")
|
|
76
|
-
.eq("card_number_encrypted", API_KEY)
|
|
77
|
-
.eq("is_active", true)
|
|
78
|
-
.single();
|
|
79
|
-
if (error || !card) {
|
|
80
|
-
console.error("API Key not found:", error?.message);
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
const { data: wallet } = await supabase
|
|
84
|
-
.from("wallets")
|
|
85
|
-
.select("balance")
|
|
86
|
-
.eq("user_id", card.user_id)
|
|
87
|
-
.single();
|
|
88
|
-
return {
|
|
89
|
-
userId: card.user_id,
|
|
90
|
-
walletBalance: Number(wallet?.balance || 0),
|
|
91
|
-
cardId: card.id,
|
|
92
|
-
cardAlias: card.alias,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
// ============================================================
|
|
96
|
-
// PUBLIC API
|
|
97
|
-
// ============================================================
|
|
98
|
-
async function listCardsRemote() {
|
|
99
|
-
const info = await resolveApiKey();
|
|
100
|
-
if (!info)
|
|
101
|
-
return [];
|
|
102
|
-
return [{ alias: info.cardAlias, balance: info.walletBalance, currency: "USD" }];
|
|
103
|
-
}
|
|
104
|
-
async function getBalanceRemote(cardAlias) {
|
|
105
|
-
const info = await resolveApiKey();
|
|
106
|
-
if (!info || info.cardAlias !== cardAlias)
|
|
107
|
-
return null;
|
|
108
|
-
return { balance: info.walletBalance, currency: "USD" };
|
|
109
|
-
}
|
|
110
|
-
async function issueTokenRemote(cardAlias, amount, merchant, ttlSeconds = 1800 // 30 minutes default
|
|
111
|
-
) {
|
|
112
|
-
// 1. Validate business rules
|
|
113
|
-
if (amount < 1) {
|
|
114
|
-
console.error("[ISSUE] Card amount must be at least $1.00");
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
if (amount > 100) {
|
|
118
|
-
console.error("[ISSUE] Card amount exceeds $100 maximum limit");
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
// 2. Resolve user from API key
|
|
122
|
-
const info = await resolveApiKey();
|
|
123
|
-
if (!info)
|
|
124
|
-
return null;
|
|
125
|
-
if (info.cardAlias !== cardAlias)
|
|
126
|
-
return null;
|
|
127
|
-
if (info.walletBalance < amount) {
|
|
128
|
-
console.error(`[ISSUE] Insufficient balance: $${info.walletBalance} < $${amount}`);
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
const tokenId = `z_jit_${crypto_1.default.randomBytes(8).toString("hex")}`;
|
|
132
|
-
// 3. Try to create a REAL Airwallex card
|
|
133
|
-
let airwallexCardId;
|
|
134
|
-
const airwallexAuth = await getAirwallexToken();
|
|
135
|
-
if (airwallexAuth) {
|
|
136
|
-
try {
|
|
137
|
-
const expiryTime = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
138
|
-
const payload = {
|
|
139
|
-
request_id: tokenId,
|
|
140
|
-
issue_to: "ORGANISATION",
|
|
141
|
-
name_on_card: "Z-ZERO AI AGENT",
|
|
142
|
-
form_factor: "VIRTUAL",
|
|
143
|
-
primary_currency: "USD",
|
|
144
|
-
valid_to: expiryTime,
|
|
145
|
-
authorization_controls: {
|
|
146
|
-
allowed_transaction_count: "SINGLE",
|
|
147
|
-
transaction_limits: {
|
|
148
|
-
currency: "USD",
|
|
149
|
-
limits: [{ amount, interval: "ALL_TIME" }],
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
const res = await fetch(`${AIRWALLEX_BASE}/issuing/cards/create`, {
|
|
154
|
-
method: "POST",
|
|
155
|
-
headers: {
|
|
156
|
-
Authorization: `Bearer ${airwallexAuth}`,
|
|
157
|
-
"Content-Type": "application/json",
|
|
158
|
-
},
|
|
159
|
-
body: JSON.stringify(payload),
|
|
160
|
-
});
|
|
161
|
-
if (res.ok) {
|
|
162
|
-
const data = await res.json();
|
|
163
|
-
airwallexCardId = data.card_id;
|
|
164
|
-
console.error(`[AIRWALLEX] ✅ Real card issued: ${data.card_id} for $${amount}`);
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
const errText = await res.text();
|
|
168
|
-
console.error("[AIRWALLEX] Card creation failed:", errText);
|
|
169
|
-
// Fall through — will still create token but without real card ID
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (err) {
|
|
173
|
-
console.error("[AIRWALLEX] Error creating card:", err);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
console.error("[AIRWALLEX] No auth token — running in mock mode");
|
|
178
|
-
}
|
|
179
|
-
const token = {
|
|
180
|
-
token: tokenId,
|
|
181
|
-
card_alias: cardAlias,
|
|
182
|
-
amount,
|
|
183
|
-
merchant,
|
|
184
|
-
created_at: Date.now(),
|
|
185
|
-
ttl_seconds: ttlSeconds,
|
|
186
|
-
used: false,
|
|
187
|
-
cancelled: false,
|
|
188
|
-
airwallex_card_id: airwallexCardId,
|
|
189
|
-
};
|
|
190
|
-
tokenStore.set(tokenId, token);
|
|
191
|
-
// 4. Pre-hold the amount in Supabase (deduct immediately, refund on cancel or underspend)
|
|
192
|
-
const newBalance = info.walletBalance - amount;
|
|
193
|
-
await supabase
|
|
194
|
-
.from("wallets")
|
|
195
|
-
.update({ balance: newBalance })
|
|
196
|
-
.eq("user_id", info.userId);
|
|
197
|
-
console.error(`[ISSUE] Token ${tokenId} created. Balance: $${info.walletBalance} → $${newBalance}`);
|
|
198
|
-
return token;
|
|
199
|
-
}
|
|
200
|
-
async function resolveTokenRemote(tokenId) {
|
|
201
|
-
const token = tokenStore.get(tokenId);
|
|
202
|
-
if (!token || token.used || token.cancelled)
|
|
203
|
-
return null;
|
|
204
|
-
const age = (Date.now() - token.created_at) / 1000;
|
|
205
|
-
if (age > token.ttl_seconds) {
|
|
206
|
-
tokenStore.delete(tokenId);
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
// Try to fetch REAL card details from Airwallex
|
|
210
|
-
if (token.airwallex_card_id) {
|
|
211
|
-
const airwallexAuth = await getAirwallexToken();
|
|
212
|
-
if (airwallexAuth) {
|
|
213
|
-
try {
|
|
214
|
-
const res = await fetch(`${AIRWALLEX_BASE}/issuing/cards/${token.airwallex_card_id}`, {
|
|
215
|
-
headers: { Authorization: `Bearer ${airwallexAuth}` },
|
|
216
|
-
});
|
|
217
|
-
if (res.ok) {
|
|
218
|
-
const data = await res.json();
|
|
219
|
-
console.error("[AIRWALLEX] ✅ Real card details fetched.");
|
|
220
|
-
return {
|
|
221
|
-
number: data.card_number || "4242424242424242",
|
|
222
|
-
exp_month: data.expiry_month || "12",
|
|
223
|
-
exp_year: data.expiry_year || "2030",
|
|
224
|
-
cvv: data.cvv || "123",
|
|
225
|
-
name: data.name_on_card || "Z-ZERO AI AGENT",
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch (err) {
|
|
230
|
-
console.error("[AIRWALLEX] Failed to fetch card details:", err);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
// Fallback to Stripe test card (demo/sandbox mode)
|
|
235
|
-
console.error("[AIRWALLEX] ⚠️ Using test card fallback (4242...)");
|
|
236
|
-
return {
|
|
237
|
-
number: "4242424242424242",
|
|
238
|
-
exp_month: "12",
|
|
239
|
-
exp_year: "2030",
|
|
240
|
-
cvv: "123",
|
|
241
|
-
name: "Z-ZERO Agent",
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
async function cancelTokenRemote(tokenId) {
|
|
245
|
-
const token = tokenStore.get(tokenId);
|
|
246
|
-
if (!token || token.used || token.cancelled) {
|
|
247
|
-
return { success: false, refunded_amount: 0 };
|
|
248
|
-
}
|
|
249
|
-
// Mark token as cancelled in memory
|
|
250
|
-
token.cancelled = true;
|
|
251
|
-
// Cancel the Airwallex card (if one was issued)
|
|
252
|
-
if (token.airwallex_card_id) {
|
|
253
|
-
const airwallexAuth = await getAirwallexToken();
|
|
254
|
-
if (airwallexAuth) {
|
|
255
|
-
try {
|
|
256
|
-
await fetch(`${AIRWALLEX_BASE}/issuing/cards/${token.airwallex_card_id}/deactivate`, {
|
|
257
|
-
method: "POST",
|
|
258
|
-
headers: { Authorization: `Bearer ${airwallexAuth}`, "Content-Type": "application/json" },
|
|
259
|
-
body: JSON.stringify({ cancellation_reason: "CANCELLED_BY_USER" }),
|
|
260
|
-
});
|
|
261
|
-
console.error(`[AIRWALLEX] Card ${token.airwallex_card_id} cancelled.`);
|
|
262
|
-
}
|
|
263
|
-
catch (err) {
|
|
264
|
-
console.error("[AIRWALLEX] Failed to cancel card:", err);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
// Refund the held amount back to wallet
|
|
269
|
-
const info = await resolveApiKey();
|
|
270
|
-
if (info) {
|
|
271
|
-
const currentBalanceRes = await supabase
|
|
272
|
-
.from("wallets")
|
|
273
|
-
.select("balance")
|
|
274
|
-
.eq("user_id", info.userId)
|
|
275
|
-
.single();
|
|
276
|
-
const currentBalance = Number(currentBalanceRes.data?.balance || 0);
|
|
277
|
-
await supabase
|
|
278
|
-
.from("wallets")
|
|
279
|
-
.update({ balance: currentBalance + token.amount })
|
|
280
|
-
.eq("user_id", info.userId);
|
|
281
|
-
console.error(`[CANCEL] Refunded $${token.amount}. New balance: $${currentBalance + token.amount}`);
|
|
282
|
-
}
|
|
283
|
-
setTimeout(() => tokenStore.delete(tokenId), 5000);
|
|
284
|
-
return { success: true, refunded_amount: token.amount };
|
|
285
|
-
}
|
|
286
|
-
async function burnTokenRemote(tokenId) {
|
|
287
|
-
const token = tokenStore.get(tokenId);
|
|
288
|
-
if (!token || token.cancelled)
|
|
289
|
-
return false;
|
|
290
|
-
token.used = true;
|
|
291
|
-
// Log the transaction — NOTE: wallet was already debited at issue time
|
|
292
|
-
const info = await resolveApiKey();
|
|
293
|
-
if (info) {
|
|
294
|
-
await supabase.from("transactions").insert({
|
|
295
|
-
card_id: info.cardId,
|
|
296
|
-
amount: token.amount,
|
|
297
|
-
merchant: token.merchant,
|
|
298
|
-
status: "SUCCESS",
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
setTimeout(() => tokenStore.delete(tokenId), 5000);
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
// Handle underspend: called when transaction amount < token amount
|
|
305
|
-
async function refundUnderspendRemote(tokenId, actualSpent) {
|
|
306
|
-
const token = tokenStore.get(tokenId);
|
|
307
|
-
if (!token)
|
|
308
|
-
return;
|
|
309
|
-
const overheld = token.amount - actualSpent;
|
|
310
|
-
if (overheld <= 0)
|
|
311
|
-
return;
|
|
312
|
-
const info = await resolveApiKey();
|
|
313
|
-
if (!info)
|
|
314
|
-
return;
|
|
315
|
-
const currentBalanceRes = await supabase
|
|
316
|
-
.from("wallets")
|
|
317
|
-
.select("balance")
|
|
318
|
-
.eq("user_id", info.userId)
|
|
319
|
-
.single();
|
|
320
|
-
const currentBalance = Number(currentBalanceRes.data?.balance || 0);
|
|
321
|
-
await supabase
|
|
322
|
-
.from("wallets")
|
|
323
|
-
.update({ balance: currentBalance + overheld })
|
|
324
|
-
.eq("user_id", info.userId);
|
|
325
|
-
console.error(`[REFUND] Underspend refunded: $${overheld} (Spent $${actualSpent} of $${token.amount})`);
|
|
326
|
-
}
|