opencard 1.0.0
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/API.md +564 -0
- package/DATABASE.md +143 -0
- package/README.md +106 -0
- package/admin/index.html +18 -0
- package/admin/package-lock.json +2663 -0
- package/admin/package.json +25 -0
- package/admin/postcss.config.js +6 -0
- package/admin/src/App.tsx +198 -0
- package/admin/src/index.css +44 -0
- package/admin/src/main.tsx +10 -0
- package/admin/src/pages/Cards.tsx +181 -0
- package/admin/src/pages/Dashboard.tsx +139 -0
- package/admin/src/pages/Transactions.tsx +223 -0
- package/admin/tailwind.config.js +27 -0
- package/admin/tsconfig.json +20 -0
- package/admin/tsconfig.tsbuildinfo +1 -0
- package/admin/vite.config.ts +17 -0
- package/drizzle.config.ts +11 -0
- package/examples/agent-client-sdk.ts +36 -0
- package/examples/agent-client.ts +111 -0
- package/examples/agent-fund-sdk.ts +35 -0
- package/package.json +41 -0
- package/sdk/README.md +139 -0
- package/sdk/package-lock.json +240 -0
- package/sdk/package.json +43 -0
- package/sdk/src/client.ts +194 -0
- package/sdk/src/errors.ts +66 -0
- package/sdk/src/index.ts +35 -0
- package/sdk/src/types.ts +138 -0
- package/sdk/src/x402.ts +158 -0
- package/sdk/tsconfig.json +20 -0
- package/src/config/env.ts +45 -0
- package/src/config/tiers.ts +51 -0
- package/src/db/index.ts +9 -0
- package/src/db/migrate.ts +16 -0
- package/src/db/schema.ts +82 -0
- package/src/index.ts +89 -0
- package/src/middleware/errorHandler.ts +27 -0
- package/src/middleware/rateLimit.ts +54 -0
- package/src/middleware/walletAuth.ts +89 -0
- package/src/middleware/x402.ts +194 -0
- package/src/routes/admin.ts +150 -0
- package/src/routes/cards.ts +120 -0
- package/src/routes/paid.ts +154 -0
- package/src/routes/public.ts +40 -0
- package/src/services/cardService.ts +395 -0
- package/src/services/kripicard.ts +128 -0
- package/src/services/walletService.ts +78 -0
- package/src/types/index.ts +128 -0
- package/src/utils/logger.ts +19 -0
- package/src/utils/pricing.ts +75 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
interface Transaction {
|
|
4
|
+
id: string;
|
|
5
|
+
walletAddress: string;
|
|
6
|
+
cardId: string | null;
|
|
7
|
+
type: string;
|
|
8
|
+
amountUsd: string;
|
|
9
|
+
cardAmountUsd: string;
|
|
10
|
+
feeUsd: string;
|
|
11
|
+
kripiCardFeeUsd: string;
|
|
12
|
+
txHash: string | null;
|
|
13
|
+
status: string;
|
|
14
|
+
errorMessage: string | null;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function Transactions({ token }: { token: string }) {
|
|
19
|
+
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
|
20
|
+
const [filter, setFilter] = useState<string>("all");
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
fetchTransactions();
|
|
25
|
+
}, [filter]);
|
|
26
|
+
|
|
27
|
+
async function fetchTransactions() {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
try {
|
|
30
|
+
const params = filter !== "all" ? `?status=${filter}` : "";
|
|
31
|
+
const res = await fetch(`/admin/api/transactions${params}`, {
|
|
32
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
33
|
+
});
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
setTransactions(data.transactions);
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const statusColors: Record<string, string> = {
|
|
45
|
+
completed: "var(--success)",
|
|
46
|
+
failed: "var(--danger)",
|
|
47
|
+
pending: "var(--warning)",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="space-y-6">
|
|
52
|
+
<div className="flex items-center justify-between">
|
|
53
|
+
<h2
|
|
54
|
+
className="text-2xl font-bold"
|
|
55
|
+
style={{ color: "var(--text-primary)" }}
|
|
56
|
+
>
|
|
57
|
+
Transactions
|
|
58
|
+
</h2>
|
|
59
|
+
<div className="flex gap-1">
|
|
60
|
+
{["all", "completed", "failed", "pending"].map((f) => (
|
|
61
|
+
<button
|
|
62
|
+
key={f}
|
|
63
|
+
onClick={() => setFilter(f)}
|
|
64
|
+
className="px-3 py-1.5 rounded-lg text-xs font-medium capitalize transition-all"
|
|
65
|
+
style={{
|
|
66
|
+
background: filter === f ? "var(--accent-glow)" : "transparent",
|
|
67
|
+
color: filter === f ? "var(--accent)" : "var(--text-muted)",
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{f}
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{loading ? (
|
|
77
|
+
<div className="flex justify-center py-12">
|
|
78
|
+
<div
|
|
79
|
+
className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
|
|
80
|
+
style={{
|
|
81
|
+
borderColor: "var(--accent)",
|
|
82
|
+
borderTopColor: "transparent",
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
) : (
|
|
87
|
+
<div
|
|
88
|
+
className="overflow-x-auto rounded-xl"
|
|
89
|
+
style={{ border: "1px solid var(--border)" }}
|
|
90
|
+
>
|
|
91
|
+
<table className="w-full text-sm">
|
|
92
|
+
<thead>
|
|
93
|
+
<tr style={{ background: "var(--bg-secondary)" }}>
|
|
94
|
+
<th
|
|
95
|
+
className="text-left px-4 py-3 font-medium"
|
|
96
|
+
style={{ color: "var(--text-muted)" }}
|
|
97
|
+
>
|
|
98
|
+
Type
|
|
99
|
+
</th>
|
|
100
|
+
<th
|
|
101
|
+
className="text-left px-4 py-3 font-medium"
|
|
102
|
+
style={{ color: "var(--text-muted)" }}
|
|
103
|
+
>
|
|
104
|
+
Wallet
|
|
105
|
+
</th>
|
|
106
|
+
<th
|
|
107
|
+
className="text-right px-4 py-3 font-medium"
|
|
108
|
+
style={{ color: "var(--text-muted)" }}
|
|
109
|
+
>
|
|
110
|
+
Charged
|
|
111
|
+
</th>
|
|
112
|
+
<th
|
|
113
|
+
className="text-right px-4 py-3 font-medium"
|
|
114
|
+
style={{ color: "var(--text-muted)" }}
|
|
115
|
+
>
|
|
116
|
+
Card Load
|
|
117
|
+
</th>
|
|
118
|
+
<th
|
|
119
|
+
className="text-right px-4 py-3 font-medium"
|
|
120
|
+
style={{ color: "var(--text-muted)" }}
|
|
121
|
+
>
|
|
122
|
+
Our Fee
|
|
123
|
+
</th>
|
|
124
|
+
<th
|
|
125
|
+
className="text-center px-4 py-3 font-medium"
|
|
126
|
+
style={{ color: "var(--text-muted)" }}
|
|
127
|
+
>
|
|
128
|
+
Status
|
|
129
|
+
</th>
|
|
130
|
+
<th
|
|
131
|
+
className="text-left px-4 py-3 font-medium"
|
|
132
|
+
style={{ color: "var(--text-muted)" }}
|
|
133
|
+
>
|
|
134
|
+
Date
|
|
135
|
+
</th>
|
|
136
|
+
</tr>
|
|
137
|
+
</thead>
|
|
138
|
+
<tbody>
|
|
139
|
+
{transactions.map((tx) => (
|
|
140
|
+
<tr
|
|
141
|
+
key={tx.id}
|
|
142
|
+
className="transition-colors"
|
|
143
|
+
style={{ borderTop: "1px solid var(--border)" }}
|
|
144
|
+
onMouseEnter={(e) =>
|
|
145
|
+
(e.currentTarget.style.background = "var(--bg-card-hover)")
|
|
146
|
+
}
|
|
147
|
+
onMouseLeave={(e) =>
|
|
148
|
+
(e.currentTarget.style.background = "transparent")
|
|
149
|
+
}
|
|
150
|
+
>
|
|
151
|
+
<td className="px-4 py-3">
|
|
152
|
+
<span
|
|
153
|
+
className="text-xs px-2 py-1 rounded-md font-medium"
|
|
154
|
+
style={{
|
|
155
|
+
background: "var(--accent-glow)",
|
|
156
|
+
color: "var(--accent)",
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
{tx.type.replace("card_", "")}
|
|
160
|
+
</span>
|
|
161
|
+
</td>
|
|
162
|
+
<td
|
|
163
|
+
className="px-4 py-3 font-mono text-xs"
|
|
164
|
+
style={{ color: "var(--text-secondary)" }}
|
|
165
|
+
>
|
|
166
|
+
{tx.walletAddress.slice(0, 6)}...
|
|
167
|
+
{tx.walletAddress.slice(-4)}
|
|
168
|
+
</td>
|
|
169
|
+
<td
|
|
170
|
+
className="px-4 py-3 text-right font-medium"
|
|
171
|
+
style={{ color: "var(--text-primary)" }}
|
|
172
|
+
>
|
|
173
|
+
${parseFloat(tx.amountUsd).toFixed(2)}
|
|
174
|
+
</td>
|
|
175
|
+
<td
|
|
176
|
+
className="px-4 py-3 text-right"
|
|
177
|
+
style={{ color: "var(--text-secondary)" }}
|
|
178
|
+
>
|
|
179
|
+
${parseFloat(tx.cardAmountUsd).toFixed(2)}
|
|
180
|
+
</td>
|
|
181
|
+
<td
|
|
182
|
+
className="px-4 py-3 text-right"
|
|
183
|
+
style={{ color: "var(--success)" }}
|
|
184
|
+
>
|
|
185
|
+
${parseFloat(tx.feeUsd).toFixed(2)}
|
|
186
|
+
</td>
|
|
187
|
+
<td className="px-4 py-3 text-center">
|
|
188
|
+
<span
|
|
189
|
+
className="text-xs px-2 py-1 rounded-full font-medium"
|
|
190
|
+
style={{
|
|
191
|
+
background: `${statusColors[tx.status]}15`,
|
|
192
|
+
color: statusColors[tx.status] || "var(--text-muted)",
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
{tx.status}
|
|
196
|
+
</span>
|
|
197
|
+
</td>
|
|
198
|
+
<td
|
|
199
|
+
className="px-4 py-3 text-xs"
|
|
200
|
+
style={{ color: "var(--text-muted)" }}
|
|
201
|
+
>
|
|
202
|
+
{new Date(tx.createdAt).toLocaleString()}
|
|
203
|
+
</td>
|
|
204
|
+
</tr>
|
|
205
|
+
))}
|
|
206
|
+
{transactions.length === 0 && (
|
|
207
|
+
<tr>
|
|
208
|
+
<td
|
|
209
|
+
colSpan={7}
|
|
210
|
+
className="px-4 py-12 text-center"
|
|
211
|
+
style={{ color: "var(--text-muted)" }}
|
|
212
|
+
>
|
|
213
|
+
No transactions found
|
|
214
|
+
</td>
|
|
215
|
+
</tr>
|
|
216
|
+
)}
|
|
217
|
+
</tbody>
|
|
218
|
+
</table>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
|
4
|
+
theme: {
|
|
5
|
+
extend: {
|
|
6
|
+
fontFamily: {
|
|
7
|
+
sans: ["Inter", "system-ui", "sans-serif"],
|
|
8
|
+
},
|
|
9
|
+
colors: {
|
|
10
|
+
brand: {
|
|
11
|
+
50: "#eef2ff",
|
|
12
|
+
100: "#e0e7ff",
|
|
13
|
+
200: "#c7d2fe",
|
|
14
|
+
300: "#a5b4fc",
|
|
15
|
+
400: "#818cf8",
|
|
16
|
+
500: "#6366f1",
|
|
17
|
+
600: "#4f46e5",
|
|
18
|
+
700: "#4338ca",
|
|
19
|
+
800: "#3730a3",
|
|
20
|
+
900: "#312e81",
|
|
21
|
+
950: "#1e1b4b",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
plugins: [],
|
|
27
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": false,
|
|
16
|
+
"noUnusedParameters": false,
|
|
17
|
+
"noFallthroughCasesInSwitch": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"]
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/app.tsx","./src/main.tsx","./src/pages/cards.tsx","./src/pages/dashboard.tsx","./src/pages/transactions.tsx"],"version":"5.9.3"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
base: "/admin/app/",
|
|
7
|
+
build: {
|
|
8
|
+
outDir: "dist",
|
|
9
|
+
},
|
|
10
|
+
server: {
|
|
11
|
+
proxy: {
|
|
12
|
+
"/admin/api": "http://localhost:3000",
|
|
13
|
+
"/health": "http://localhost:3000",
|
|
14
|
+
"/pricing": "http://localhost:3000",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Agent using @opencard/sdk
|
|
3
|
+
*
|
|
4
|
+
* Compare with agent-client.ts (raw x402 protocol) — same result, 5 lines.
|
|
5
|
+
*/
|
|
6
|
+
import { OpenCardClient } from "@opencardsdk/sdk";
|
|
7
|
+
import "dotenv/config";
|
|
8
|
+
|
|
9
|
+
const client = new OpenCardClient({
|
|
10
|
+
privateKey: process.env.WALLET_KEY as `0x${string}`,
|
|
11
|
+
baseUrl: process.env.OPENCARD_API_URL || "http://localhost:3000",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log(`Wallet: ${client.address}`);
|
|
15
|
+
|
|
16
|
+
// One line — SDK handles 402 → USDC payment → retry automatically
|
|
17
|
+
const result = await client.createCard({
|
|
18
|
+
amount: 10,
|
|
19
|
+
nameOnCard: "AI AGENT",
|
|
20
|
+
email: "agent@example.com",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
console.log("Card created:", {
|
|
24
|
+
cardId: result.card.cardId,
|
|
25
|
+
status: result.card.status,
|
|
26
|
+
balance: result.card.balance,
|
|
27
|
+
lastFour: result.card.lastFour,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (result.cardDetails) {
|
|
31
|
+
console.log("Card details:", {
|
|
32
|
+
number: result.cardDetails.cardNumber,
|
|
33
|
+
expiry: `${result.cardDetails.expiryMonth}/${result.cardDetails.expiryYear}`,
|
|
34
|
+
cvv: result.cardDetails.cvv,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createWalletClient, http, parseEther, encodeFunctionData } from "viem";
|
|
2
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
import { base } from "viem/chains";
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
|
|
6
|
+
// Mock AI Agent Wallet
|
|
7
|
+
// In reality, this would be the agent's actual wallet
|
|
8
|
+
const account = privateKeyToAccount(
|
|
9
|
+
(process.env.TEST_WALLET_KEY as `0x${string}`) ||
|
|
10
|
+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const client = createWalletClient({
|
|
14
|
+
account,
|
|
15
|
+
chain: base,
|
|
16
|
+
transport: http(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const USDC_ABI = [
|
|
20
|
+
{
|
|
21
|
+
inputs: [
|
|
22
|
+
{ name: "to", type: "address" },
|
|
23
|
+
{ name: "amount", type: "uint256" },
|
|
24
|
+
],
|
|
25
|
+
name: "transfer",
|
|
26
|
+
outputs: [{ name: "", type: "bool" }],
|
|
27
|
+
stateMutability: "nonpayable",
|
|
28
|
+
type: "function",
|
|
29
|
+
},
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
async function purchaseCard() {
|
|
33
|
+
const endpoint = "http://localhost:3000/cards/create/tier/10";
|
|
34
|
+
const body = JSON.stringify({
|
|
35
|
+
nameOnCard: "AI AGENT",
|
|
36
|
+
email: "agent@example.com",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.log("🤖 Agent: Requesting card...");
|
|
40
|
+
|
|
41
|
+
// 1. Initial Request (Expect 402)
|
|
42
|
+
const res = await fetch(endpoint, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
body,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (res.status === 402) {
|
|
49
|
+
const challenge = await res.json();
|
|
50
|
+
console.log("🔴 Server: 402 Payment Required");
|
|
51
|
+
console.log(" Details:", JSON.stringify(challenge, null, 2));
|
|
52
|
+
|
|
53
|
+
// 2. Parse Payment Requirements
|
|
54
|
+
const paymentReq = challenge.accepts[0];
|
|
55
|
+
const amount = BigInt(paymentReq.maxAmountRequired);
|
|
56
|
+
const to = paymentReq.payTo as `0x${string}`;
|
|
57
|
+
const token = paymentReq.asset as `0x${string}`;
|
|
58
|
+
|
|
59
|
+
console.log(
|
|
60
|
+
`🤖 Agent: Detected payment requirement: ${amount} units of ${token} on Base`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// 3. Execute Transaction (Mocked for this example, or real if keys provided)
|
|
64
|
+
// In production, the agent signs and broadcasts the USDC transfer
|
|
65
|
+
// const hash = await client.writeContract({...});
|
|
66
|
+
const mockTxHash =
|
|
67
|
+
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
|
|
68
|
+
|
|
69
|
+
console.log(`🤖 Agent: Payment sent! Tx Hash: ${mockTxHash}`);
|
|
70
|
+
|
|
71
|
+
// 4. Construct x402 Payment Proof
|
|
72
|
+
const paymentProof = {
|
|
73
|
+
scheme: "exact",
|
|
74
|
+
network: paymentReq.network,
|
|
75
|
+
payload: {
|
|
76
|
+
authorization: {
|
|
77
|
+
from: account.address,
|
|
78
|
+
to,
|
|
79
|
+
value: amount.toString(),
|
|
80
|
+
validAfter: "0",
|
|
81
|
+
validBefore: "9999999999",
|
|
82
|
+
nonce: "0",
|
|
83
|
+
},
|
|
84
|
+
signature: "0x...", // Signature of the authorization
|
|
85
|
+
txHash: mockTxHash, // The transaction hash
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const encodedProof = Buffer.from(JSON.stringify(paymentProof)).toString(
|
|
90
|
+
"base64",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// 5. Retry Request with Payment Header
|
|
94
|
+
console.log("🤖 Agent: Retrying request with payment proof...");
|
|
95
|
+
const paidRes = await fetch(endpoint, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
"X-Payment": encodedProof,
|
|
100
|
+
},
|
|
101
|
+
body,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const result = await paidRes.json();
|
|
105
|
+
console.log("🟢 Server Response:", paidRes.status, result);
|
|
106
|
+
} else {
|
|
107
|
+
console.log("Response:", res.status, await res.json());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
purchaseCard().catch(console.error);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fund an existing card using @opencard/sdk
|
|
3
|
+
*/
|
|
4
|
+
import { OpenCardClient, InsufficientBalanceError, ApiError } from "@opencardsdk/sdk";
|
|
5
|
+
import "dotenv/config";
|
|
6
|
+
|
|
7
|
+
const client = new OpenCardClient({
|
|
8
|
+
privateKey: process.env.WALLET_KEY as `0x${string}`,
|
|
9
|
+
baseUrl: process.env.OPENCARD_API_URL || "http://localhost:3000",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Check available tiers first
|
|
14
|
+
const tiers = await client.getTiers();
|
|
15
|
+
console.log(
|
|
16
|
+
"Funding tiers:",
|
|
17
|
+
tiers.funding.map((t) => `$${t.fundAmount} (cost: $${t.totalCost})`),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// Fund a card
|
|
21
|
+
const result = await client.fundCard({
|
|
22
|
+
amount: 25,
|
|
23
|
+
cardId: "your-card-id-here",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log("Card funded:", result);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof InsufficientBalanceError) {
|
|
29
|
+
console.error("Not enough USDC:", error.message);
|
|
30
|
+
} else if (error instanceof ApiError) {
|
|
31
|
+
console.error(`API error ${error.status}:`, error.body);
|
|
32
|
+
} else {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "x402-Powered Virtual Card Issuance API for AI Agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx watch src/index.ts",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"start": "node dist/index.js",
|
|
11
|
+
"db:generate": "drizzle-kit generate",
|
|
12
|
+
"db:push": "drizzle-kit push",
|
|
13
|
+
"db:migrate": "tsx src/db/migrate.ts",
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"express": "^4.21.0",
|
|
18
|
+
"@x402/express": "latest",
|
|
19
|
+
"@x402/evm": "latest",
|
|
20
|
+
"@x402/core": "latest",
|
|
21
|
+
"@coinbase/x402": "latest",
|
|
22
|
+
"viem": "^2.0.0",
|
|
23
|
+
"drizzle-orm": "^0.36.0",
|
|
24
|
+
"@neondatabase/serverless": "^0.10.0",
|
|
25
|
+
"zod": "^3.23.0",
|
|
26
|
+
"pino": "^9.0.0",
|
|
27
|
+
"pino-pretty": "^11.0.0",
|
|
28
|
+
"dotenv": "^16.4.0",
|
|
29
|
+
"express-rate-limit": "^7.0.0",
|
|
30
|
+
"helmet": "^8.0.0",
|
|
31
|
+
"cors": "^2.8.5"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.5.0",
|
|
35
|
+
"tsx": "^4.0.0",
|
|
36
|
+
"drizzle-kit": "^0.28.0",
|
|
37
|
+
"@types/express": "^5.0.0",
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"@types/cors": "^2.8.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/sdk/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @opencardsdk/sdk
|
|
2
|
+
|
|
3
|
+
Client SDK for OpenCard — create virtual cards with USDC payments via the x402 protocol.
|
|
4
|
+
|
|
5
|
+
Wraps the raw x402 flow (402 → parse challenge → pay USDC → retry with proof) into one-liner methods.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @opencardsdk/sdk viem
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { OpenCardClient } from '@opencardsdk/sdk';
|
|
17
|
+
|
|
18
|
+
const client = new OpenCardClient({
|
|
19
|
+
privateKey: '0x...', // or pass a viem WalletClient
|
|
20
|
+
baseUrl: 'https://api.opencard.dev',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// One line — SDK handles payment automatically
|
|
24
|
+
const card = await client.createCard({
|
|
25
|
+
amount: 10, // $10 tier
|
|
26
|
+
nameOnCard: 'AI AGENT',
|
|
27
|
+
email: 'agent@example.com',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log(card.cardDetails); // { cardNumber, cvv, expiry, ... }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
### `new OpenCardClient(config)`
|
|
36
|
+
|
|
37
|
+
| Option | Type | Required | Description |
|
|
38
|
+
|--------|------|----------|-------------|
|
|
39
|
+
| `privateKey` | `0x${string}` | One of | Hex private key — SDK creates wallet internally |
|
|
40
|
+
| `walletClient` | `WalletClient` | One of | Your own viem WalletClient (takes precedence) |
|
|
41
|
+
| `baseUrl` | `string` | No | API URL (default: `https://api.opencard.dev`) |
|
|
42
|
+
| `rpcUrl` | `string` | No | Base RPC URL (default: public RPC) |
|
|
43
|
+
| `timeout` | `number` | No | Request timeout in ms (default: 60000) |
|
|
44
|
+
|
|
45
|
+
### `client.createCard(params): Promise<CardResult>`
|
|
46
|
+
|
|
47
|
+
Create a virtual card. Pays USDC automatically.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const result = await client.createCard({
|
|
51
|
+
amount: 50, // 10 | 25 | 50 | 100 | 200 | 500
|
|
52
|
+
nameOnCard: 'AI AGENT',
|
|
53
|
+
email: 'agent@example.com',
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `client.fundCard(params): Promise<FundResult>`
|
|
58
|
+
|
|
59
|
+
Fund an existing card.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const result = await client.fundCard({
|
|
63
|
+
amount: 25,
|
|
64
|
+
cardId: 'card-uuid',
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `client.getTiers(): Promise<TiersResponse>`
|
|
69
|
+
|
|
70
|
+
Get pricing tiers and fee breakdown (no payment required).
|
|
71
|
+
|
|
72
|
+
### `client.health(): Promise<{ status: string }>`
|
|
73
|
+
|
|
74
|
+
Check if the OpenCard API is reachable.
|
|
75
|
+
|
|
76
|
+
### `client.address: 0x${string}`
|
|
77
|
+
|
|
78
|
+
The wallet address being used for payments.
|
|
79
|
+
|
|
80
|
+
## Error Handling
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import {
|
|
84
|
+
OpenCardClient,
|
|
85
|
+
InsufficientBalanceError,
|
|
86
|
+
PaymentError,
|
|
87
|
+
ApiError,
|
|
88
|
+
TimeoutError,
|
|
89
|
+
} from '@opencardsdk/sdk';
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const card = await client.createCard({ ... });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof InsufficientBalanceError) {
|
|
95
|
+
console.log(`Need ${error.required}, have ${error.available}`);
|
|
96
|
+
} else if (error instanceof PaymentError) {
|
|
97
|
+
console.log(`Payment failed: ${error.message}, tx: ${error.txHash}`);
|
|
98
|
+
} else if (error instanceof ApiError) {
|
|
99
|
+
console.log(`Server error ${error.status}:`, error.body);
|
|
100
|
+
} else if (error instanceof TimeoutError) {
|
|
101
|
+
console.log('Request timed out');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Advanced: Low-Level x402 Utilities
|
|
107
|
+
|
|
108
|
+
For custom integrations, the x402 protocol helpers are exported directly:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import {
|
|
112
|
+
parseChallenge,
|
|
113
|
+
checkBalance,
|
|
114
|
+
executePayment,
|
|
115
|
+
buildPaymentProof,
|
|
116
|
+
handleX402Payment,
|
|
117
|
+
} from '@opencardsdk/sdk';
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## How It Works
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
Your Code SDK OpenCard API Base Chain
|
|
124
|
+
| | | |
|
|
125
|
+
|-- createCard ->| | |
|
|
126
|
+
| |--- POST /create/tier -->| |
|
|
127
|
+
| |<-- 402 + challenge -----| |
|
|
128
|
+
| | | |
|
|
129
|
+
| |--- USDC.transfer() -----|-------- tx ------->|
|
|
130
|
+
| |<-- txHash --------------|-------- receipt ---|
|
|
131
|
+
| | | |
|
|
132
|
+
| |--- POST + X-Payment --->| |
|
|
133
|
+
| |<-- 201 + card details --| |
|
|
134
|
+
|<- CardResult --| | |
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|