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.
Files changed (52) hide show
  1. package/API.md +564 -0
  2. package/DATABASE.md +143 -0
  3. package/README.md +106 -0
  4. package/admin/index.html +18 -0
  5. package/admin/package-lock.json +2663 -0
  6. package/admin/package.json +25 -0
  7. package/admin/postcss.config.js +6 -0
  8. package/admin/src/App.tsx +198 -0
  9. package/admin/src/index.css +44 -0
  10. package/admin/src/main.tsx +10 -0
  11. package/admin/src/pages/Cards.tsx +181 -0
  12. package/admin/src/pages/Dashboard.tsx +139 -0
  13. package/admin/src/pages/Transactions.tsx +223 -0
  14. package/admin/tailwind.config.js +27 -0
  15. package/admin/tsconfig.json +20 -0
  16. package/admin/tsconfig.tsbuildinfo +1 -0
  17. package/admin/vite.config.ts +17 -0
  18. package/drizzle.config.ts +11 -0
  19. package/examples/agent-client-sdk.ts +36 -0
  20. package/examples/agent-client.ts +111 -0
  21. package/examples/agent-fund-sdk.ts +35 -0
  22. package/package.json +41 -0
  23. package/sdk/README.md +139 -0
  24. package/sdk/package-lock.json +240 -0
  25. package/sdk/package.json +43 -0
  26. package/sdk/src/client.ts +194 -0
  27. package/sdk/src/errors.ts +66 -0
  28. package/sdk/src/index.ts +35 -0
  29. package/sdk/src/types.ts +138 -0
  30. package/sdk/src/x402.ts +158 -0
  31. package/sdk/tsconfig.json +20 -0
  32. package/src/config/env.ts +45 -0
  33. package/src/config/tiers.ts +51 -0
  34. package/src/db/index.ts +9 -0
  35. package/src/db/migrate.ts +16 -0
  36. package/src/db/schema.ts +82 -0
  37. package/src/index.ts +89 -0
  38. package/src/middleware/errorHandler.ts +27 -0
  39. package/src/middleware/rateLimit.ts +54 -0
  40. package/src/middleware/walletAuth.ts +89 -0
  41. package/src/middleware/x402.ts +194 -0
  42. package/src/routes/admin.ts +150 -0
  43. package/src/routes/cards.ts +120 -0
  44. package/src/routes/paid.ts +154 -0
  45. package/src/routes/public.ts +40 -0
  46. package/src/services/cardService.ts +395 -0
  47. package/src/services/kripicard.ts +128 -0
  48. package/src/services/walletService.ts +78 -0
  49. package/src/types/index.ts +128 -0
  50. package/src/utils/logger.ts +19 -0
  51. package/src/utils/pricing.ts +75 -0
  52. package/tsconfig.json +21 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "opencard-admin",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.0",
13
+ "react-dom": "^18.3.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^18.3.0",
17
+ "@types/react-dom": "^18.3.0",
18
+ "@vitejs/plugin-react": "^4.3.0",
19
+ "autoprefixer": "^10.4.0",
20
+ "postcss": "^8.4.0",
21
+ "tailwindcss": "^3.4.0",
22
+ "typescript": "^5.5.0",
23
+ "vite": "^5.4.0"
24
+ }
25
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,198 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import Dashboard from "./pages/Dashboard";
3
+ import Transactions from "./pages/Transactions";
4
+ import Cards from "./pages/Cards";
5
+
6
+ type Page = "dashboard" | "transactions" | "cards";
7
+
8
+ interface Stats {
9
+ cards: { totalCards: number; activeCards: number; frozenCards: number };
10
+ transactions: {
11
+ totalTransactions: number;
12
+ completedTransactions: number;
13
+ failedTransactions: number;
14
+ pendingTransactions: number;
15
+ totalRevenue: string;
16
+ totalVolume: string;
17
+ };
18
+ wallets: { totalWallets: number };
19
+ }
20
+
21
+ function App() {
22
+ const [page, setPage] = useState<Page>("dashboard");
23
+ const [token, setToken] = useState(
24
+ () => localStorage.getItem("opencard_admin_token") || "",
25
+ );
26
+ const [authed, setAuthed] = useState(false);
27
+ const [stats, setStats] = useState<Stats | null>(null);
28
+ const [loginInput, setLoginInput] = useState("");
29
+
30
+ const apiBase = "/admin";
31
+
32
+ const fetchStats = useCallback(async () => {
33
+ try {
34
+ const res = await fetch(`${apiBase}/api/stats`, {
35
+ headers: { Authorization: `Bearer ${token}` },
36
+ });
37
+ if (res.ok) {
38
+ const data = await res.json();
39
+ setStats(data);
40
+ setAuthed(true);
41
+ } else {
42
+ setAuthed(false);
43
+ localStorage.removeItem("opencard_admin_token");
44
+ }
45
+ } catch {
46
+ // server not reachable
47
+ }
48
+ }, [token]);
49
+
50
+ useEffect(() => {
51
+ if (token) fetchStats();
52
+ }, [token, fetchStats]);
53
+
54
+ // Auto-refresh stats every 30s
55
+ useEffect(() => {
56
+ if (!authed) return;
57
+ const interval = setInterval(fetchStats, 30000);
58
+ return () => clearInterval(interval);
59
+ }, [authed, fetchStats]);
60
+
61
+ const handleLogin = (e: React.FormEvent) => {
62
+ e.preventDefault();
63
+ localStorage.setItem("opencard_admin_token", loginInput);
64
+ setToken(loginInput);
65
+ };
66
+
67
+ if (!authed) {
68
+ return (
69
+ <div
70
+ className="min-h-screen flex items-center justify-center"
71
+ style={{ background: "var(--bg-primary)" }}
72
+ >
73
+ <form
74
+ onSubmit={handleLogin}
75
+ className="w-full max-w-sm p-8 rounded-2xl"
76
+ style={{
77
+ background: "var(--bg-card)",
78
+ border: "1px solid var(--border)",
79
+ }}
80
+ >
81
+ <div className="flex items-center gap-3 mb-8">
82
+ <div
83
+ className="w-10 h-10 rounded-xl flex items-center justify-center text-xl font-bold"
84
+ style={{
85
+ background: "linear-gradient(135deg, #818cf8, #6366f1)",
86
+ }}
87
+ >
88
+ O
89
+ </div>
90
+ <div>
91
+ <h1
92
+ className="text-xl font-bold"
93
+ style={{ color: "var(--text-primary)" }}
94
+ >
95
+ OpenCard
96
+ </h1>
97
+ <p className="text-xs" style={{ color: "var(--text-muted)" }}>
98
+ Admin Dashboard
99
+ </p>
100
+ </div>
101
+ </div>
102
+ <input
103
+ type="password"
104
+ value={loginInput}
105
+ onChange={(e) => setLoginInput(e.target.value)}
106
+ placeholder="Admin secret..."
107
+ className="w-full px-4 py-3 rounded-lg mb-4 outline-none text-sm transition-all focus:ring-2"
108
+ style={{
109
+ background: "var(--bg-secondary)",
110
+ border: "1px solid var(--border)",
111
+ color: "var(--text-primary)",
112
+ }}
113
+ />
114
+ <button
115
+ type="submit"
116
+ className="w-full py-3 rounded-lg font-semibold text-sm text-white transition-all hover:opacity-90"
117
+ style={{ background: "linear-gradient(135deg, #818cf8, #6366f1)" }}
118
+ >
119
+ Sign In
120
+ </button>
121
+ </form>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ return (
127
+ <div className="min-h-screen" style={{ background: "var(--bg-primary)" }}>
128
+ {/* Header */}
129
+ <header
130
+ className="sticky top-0 z-50 px-6 py-4 flex items-center justify-between"
131
+ style={{
132
+ background: "rgba(15, 15, 35, 0.85)",
133
+ backdropFilter: "blur(12px)",
134
+ borderBottom: "1px solid var(--border)",
135
+ }}
136
+ >
137
+ <div className="flex items-center gap-3">
138
+ <div
139
+ className="w-9 h-9 rounded-xl flex items-center justify-center text-lg font-bold text-white"
140
+ style={{ background: "linear-gradient(135deg, #818cf8, #6366f1)" }}
141
+ >
142
+ O
143
+ </div>
144
+ <span
145
+ className="text-lg font-bold"
146
+ style={{ color: "var(--text-primary)" }}
147
+ >
148
+ OpenCard
149
+ </span>
150
+ <span
151
+ className="text-xs px-2 py-0.5 rounded-full"
152
+ style={{ background: "var(--accent-glow)", color: "var(--accent)" }}
153
+ >
154
+ Admin
155
+ </span>
156
+ </div>
157
+ <nav className="flex gap-1">
158
+ {(["dashboard", "transactions", "cards"] as Page[]).map((p) => (
159
+ <button
160
+ key={p}
161
+ onClick={() => setPage(p)}
162
+ className="px-4 py-2 rounded-lg text-sm font-medium capitalize transition-all"
163
+ style={{
164
+ background: page === p ? "var(--accent-glow)" : "transparent",
165
+ color: page === p ? "var(--accent)" : "var(--text-secondary)",
166
+ }}
167
+ >
168
+ {p}
169
+ </button>
170
+ ))}
171
+ </nav>
172
+ <button
173
+ onClick={() => {
174
+ localStorage.removeItem("opencard_admin_token");
175
+ setAuthed(false);
176
+ setToken("");
177
+ }}
178
+ className="text-xs px-3 py-1.5 rounded-lg transition-all hover:opacity-80"
179
+ style={{
180
+ background: "rgba(248, 113, 113, 0.1)",
181
+ color: "var(--danger)",
182
+ }}
183
+ >
184
+ Logout
185
+ </button>
186
+ </header>
187
+
188
+ {/* Content */}
189
+ <main className="p-6 max-w-7xl mx-auto">
190
+ {page === "dashboard" && <Dashboard stats={stats} />}
191
+ {page === "transactions" && <Transactions token={token} />}
192
+ {page === "cards" && <Cards token={token} />}
193
+ </main>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ export default App;
@@ -0,0 +1,44 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --bg-primary: #0f0f23;
7
+ --bg-secondary: #1a1a3e;
8
+ --bg-card: #1e1e45;
9
+ --bg-card-hover: #252563;
10
+ --accent: #818cf8;
11
+ --accent-glow: rgba(129, 140, 248, 0.15);
12
+ --text-primary: #e2e8f0;
13
+ --text-secondary: #94a3b8;
14
+ --text-muted: #64748b;
15
+ --border: rgba(129, 140, 248, 0.12);
16
+ --success: #34d399;
17
+ --danger: #f87171;
18
+ --warning: #fbbf24;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ font-family: "Inter", system-ui, sans-serif;
29
+ background: var(--bg-primary);
30
+ color: var(--text-primary);
31
+ min-height: 100vh;
32
+ }
33
+
34
+ /* Scrollbar */
35
+ ::-webkit-scrollbar {
36
+ width: 6px;
37
+ }
38
+ ::-webkit-scrollbar-track {
39
+ background: var(--bg-secondary);
40
+ }
41
+ ::-webkit-scrollbar-thumb {
42
+ background: var(--accent);
43
+ border-radius: 3px;
44
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App";
4
+ import "./index.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root")!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,181 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ interface Card {
4
+ id: string;
5
+ walletAddress: string;
6
+ kripiCardId: string;
7
+ nameOnCard: string;
8
+ email: string;
9
+ initialAmountUsd: string;
10
+ currentBalanceUsd: string | null;
11
+ status: string;
12
+ createdAt: string;
13
+ }
14
+
15
+ export default function Cards({ token }: { token: string }) {
16
+ const [cards, setCards] = useState<Card[]>([]);
17
+ const [loading, setLoading] = useState(true);
18
+ const [search, setSearch] = useState("");
19
+
20
+ useEffect(() => {
21
+ fetchCards();
22
+ }, []);
23
+
24
+ async function fetchCards() {
25
+ setLoading(true);
26
+ try {
27
+ const res = await fetch(`/admin/api/cards`, {
28
+ headers: { Authorization: `Bearer ${token}` },
29
+ });
30
+ if (res.ok) {
31
+ const data = await res.json();
32
+ setCards(data.cards);
33
+ }
34
+ } catch {
35
+ // ignore
36
+ }
37
+ setLoading(false);
38
+ }
39
+
40
+ const filtered = cards.filter(
41
+ (c) =>
42
+ c.walletAddress.toLowerCase().includes(search.toLowerCase()) ||
43
+ c.kripiCardId.toLowerCase().includes(search.toLowerCase()) ||
44
+ c.nameOnCard.toLowerCase().includes(search.toLowerCase()),
45
+ );
46
+
47
+ const statusColors: Record<string, string> = {
48
+ active: "var(--success)",
49
+ frozen: "var(--warning)",
50
+ depleted: "var(--text-muted)",
51
+ };
52
+
53
+ return (
54
+ <div className="space-y-6">
55
+ <div className="flex items-center justify-between">
56
+ <h2
57
+ className="text-2xl font-bold"
58
+ style={{ color: "var(--text-primary)" }}
59
+ >
60
+ Cards
61
+ </h2>
62
+ <input
63
+ type="text"
64
+ value={search}
65
+ onChange={(e) => setSearch(e.target.value)}
66
+ placeholder="Search by wallet, card ID, or name..."
67
+ className="px-4 py-2 rounded-lg text-sm outline-none w-80 transition-all focus:ring-2"
68
+ style={{
69
+ background: "var(--bg-secondary)",
70
+ border: "1px solid var(--border)",
71
+ color: "var(--text-primary)",
72
+ }}
73
+ />
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 className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
88
+ {filtered.map((card) => (
89
+ <div
90
+ key={card.id}
91
+ className="p-5 rounded-xl transition-all hover:scale-[1.01]"
92
+ style={{
93
+ background: "var(--bg-card)",
94
+ border: "1px solid var(--border)",
95
+ }}
96
+ >
97
+ <div className="flex items-center justify-between mb-4">
98
+ <span
99
+ className="text-sm font-bold"
100
+ style={{ color: "var(--text-primary)" }}
101
+ >
102
+ {card.nameOnCard}
103
+ </span>
104
+ <span
105
+ className="text-xs px-2 py-1 rounded-full font-medium capitalize"
106
+ style={{
107
+ background: `${statusColors[card.status] || "var(--text-muted)"}15`,
108
+ color: statusColors[card.status] || "var(--text-muted)",
109
+ }}
110
+ >
111
+ {card.status}
112
+ </span>
113
+ </div>
114
+
115
+ <div className="space-y-2 text-sm">
116
+ <div className="flex justify-between">
117
+ <span style={{ color: "var(--text-muted)" }}>
118
+ KripiCard ID
119
+ </span>
120
+ <span
121
+ className="font-mono text-xs"
122
+ style={{ color: "var(--text-secondary)" }}
123
+ >
124
+ {card.kripiCardId}
125
+ </span>
126
+ </div>
127
+ <div className="flex justify-between">
128
+ <span style={{ color: "var(--text-muted)" }}>Balance</span>
129
+ <span
130
+ className="font-bold"
131
+ style={{ color: "var(--success)" }}
132
+ >
133
+ $
134
+ {card.currentBalanceUsd
135
+ ? parseFloat(card.currentBalanceUsd).toFixed(2)
136
+ : "—"}
137
+ </span>
138
+ </div>
139
+ <div className="flex justify-between">
140
+ <span style={{ color: "var(--text-muted)" }}>
141
+ Initial Load
142
+ </span>
143
+ <span style={{ color: "var(--text-secondary)" }}>
144
+ ${parseFloat(card.initialAmountUsd).toFixed(2)}
145
+ </span>
146
+ </div>
147
+ <div className="flex justify-between">
148
+ <span style={{ color: "var(--text-muted)" }}>Wallet</span>
149
+ <span
150
+ className="font-mono text-xs"
151
+ style={{ color: "var(--text-secondary)" }}
152
+ >
153
+ {card.walletAddress.slice(0, 6)}...
154
+ {card.walletAddress.slice(-4)}
155
+ </span>
156
+ </div>
157
+ <div className="flex justify-between">
158
+ <span style={{ color: "var(--text-muted)" }}>Created</span>
159
+ <span
160
+ className="text-xs"
161
+ style={{ color: "var(--text-muted)" }}
162
+ >
163
+ {new Date(card.createdAt).toLocaleDateString()}
164
+ </span>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ ))}
169
+ {filtered.length === 0 && (
170
+ <div
171
+ className="col-span-full py-12 text-center"
172
+ style={{ color: "var(--text-muted)" }}
173
+ >
174
+ No cards found
175
+ </div>
176
+ )}
177
+ </div>
178
+ )}
179
+ </div>
180
+ );
181
+ }
@@ -0,0 +1,139 @@
1
+ interface Stats {
2
+ cards: { totalCards: number; activeCards: number; frozenCards: number };
3
+ transactions: {
4
+ totalTransactions: number;
5
+ completedTransactions: number;
6
+ failedTransactions: number;
7
+ pendingTransactions: number;
8
+ totalRevenue: string;
9
+ totalVolume: string;
10
+ };
11
+ wallets: { totalWallets: number };
12
+ }
13
+
14
+ function StatCard({
15
+ label,
16
+ value,
17
+ accent,
18
+ }: {
19
+ label: string;
20
+ value: string | number;
21
+ accent?: string;
22
+ }) {
23
+ return (
24
+ <div
25
+ className="p-5 rounded-xl transition-all hover:scale-[1.02]"
26
+ style={{
27
+ background: "var(--bg-card)",
28
+ border: "1px solid var(--border)",
29
+ }}
30
+ >
31
+ <p
32
+ className="text-xs font-medium mb-1 uppercase tracking-wider"
33
+ style={{ color: "var(--text-muted)" }}
34
+ >
35
+ {label}
36
+ </p>
37
+ <p
38
+ className="text-2xl font-bold"
39
+ style={{ color: accent || "var(--text-primary)" }}
40
+ >
41
+ {value}
42
+ </p>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export default function Dashboard({ stats }: { stats: Stats | null }) {
48
+ if (!stats) {
49
+ return (
50
+ <div className="flex items-center justify-center py-20">
51
+ <div
52
+ className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
53
+ style={{
54
+ borderColor: "var(--accent)",
55
+ borderTopColor: "transparent",
56
+ }}
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <div className="space-y-6">
64
+ <h2
65
+ className="text-2xl font-bold"
66
+ style={{ color: "var(--text-primary)" }}
67
+ >
68
+ Dashboard
69
+ </h2>
70
+
71
+ {/* Alert for failed transactions */}
72
+ {stats.transactions.failedTransactions > 0 && (
73
+ <div
74
+ className="p-4 rounded-xl flex items-center gap-3"
75
+ style={{
76
+ background: "rgba(248, 113, 113, 0.1)",
77
+ border: "1px solid rgba(248, 113, 113, 0.2)",
78
+ }}
79
+ >
80
+ <span className="text-lg">⚠️</span>
81
+ <div>
82
+ <p
83
+ className="text-sm font-semibold"
84
+ style={{ color: "var(--danger)" }}
85
+ >
86
+ {stats.transactions.failedTransactions} failed transaction(s)
87
+ </p>
88
+ <p className="text-xs" style={{ color: "var(--text-muted)" }}>
89
+ Agents may have paid but not received their cards. Check
90
+ Transactions tab.
91
+ </p>
92
+ </div>
93
+ </div>
94
+ )}
95
+
96
+ {/* Stats Grid */}
97
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
98
+ <StatCard label="Total Cards" value={stats.cards.totalCards} />
99
+ <StatCard
100
+ label="Active Cards"
101
+ value={stats.cards.activeCards}
102
+ accent="var(--success)"
103
+ />
104
+ <StatCard
105
+ label="Frozen Cards"
106
+ value={stats.cards.frozenCards}
107
+ accent="var(--warning)"
108
+ />
109
+ <StatCard
110
+ label="Total Wallets"
111
+ value={stats.wallets.totalWallets}
112
+ accent="var(--accent)"
113
+ />
114
+ </div>
115
+
116
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
117
+ <StatCard
118
+ label="Total Revenue"
119
+ value={`$${parseFloat(stats.transactions.totalRevenue).toFixed(2)}`}
120
+ accent="var(--success)"
121
+ />
122
+ <StatCard
123
+ label="Total Volume"
124
+ value={`$${parseFloat(stats.transactions.totalVolume).toFixed(2)}`}
125
+ />
126
+ <StatCard
127
+ label="Completed Txns"
128
+ value={stats.transactions.completedTransactions}
129
+ accent="var(--success)"
130
+ />
131
+ <StatCard
132
+ label="Pending Txns"
133
+ value={stats.transactions.pendingTransactions}
134
+ accent="var(--warning)"
135
+ />
136
+ </div>
137
+ </div>
138
+ );
139
+ }