run402 1.27.0 → 1.29.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/cli.mjs CHANGED
@@ -39,6 +39,8 @@ Commands:
39
39
  message Send messages to Run402 developers
40
40
  auth Manage project user authentication (magic link, passwords, settings)
41
41
  sender-domain Manage custom email sender domain (register, status, remove)
42
+ billing Email billing accounts, Stripe tier checkout, email packs
43
+ contracts KMS contract wallets ($0.04/day rental + $0.000005/sign)
42
44
  agent Manage agent identity (contact info)
43
45
 
44
46
  Run 'run402 <command> --help' for detailed usage of each command.
@@ -171,6 +173,16 @@ switch (cmd) {
171
173
  await run(sub, rest);
172
174
  break;
173
175
  }
176
+ case "billing": {
177
+ const { run } = await import("./lib/billing.mjs");
178
+ await run(sub, rest);
179
+ break;
180
+ }
181
+ case "contracts": {
182
+ const { run } = await import("./lib/contracts.mjs");
183
+ await run(sub, rest);
184
+ break;
185
+ }
174
186
  default:
175
187
  console.error(`Unknown command: ${cmd}\n`);
176
188
  console.log(HELP);
@@ -0,0 +1,169 @@
1
+ import { API } from "./config.mjs";
2
+
3
+ const HELP = `run402 billing — Email billing accounts, Stripe tier checkout, email packs
4
+
5
+ Usage:
6
+ run402 billing <subcommand> [args...]
7
+
8
+ Subcommands:
9
+ create-email <email> Create an email billing account
10
+ link-wallet <account_id> <wallet> Link a wallet to an email account
11
+ tier-checkout <tier> [--email <e> | --wallet <w>] Stripe tier checkout
12
+ buy-pack [--email <e> | --wallet <w>] Buy \$5 email pack (10,000 emails)
13
+ auto-recharge <account_id> <on|off> [--threshold <n>]
14
+ balance <identifier> Balance by email or wallet (0x...)
15
+ history <identifier> [--limit <n>] Ledger history by email or wallet
16
+
17
+ Examples:
18
+ run402 billing create-email user@example.com
19
+ run402 billing tier-checkout hobby --email user@example.com
20
+ run402 billing buy-pack --wallet 0x1234...
21
+ run402 billing auto-recharge acct_abc on --threshold 2000
22
+ run402 billing balance user@example.com
23
+ `;
24
+
25
+ function parseFlag(args, flag) {
26
+ for (let i = 0; i < args.length; i++) {
27
+ if (args[i] === flag && args[i + 1]) return args[i + 1];
28
+ }
29
+ return null;
30
+ }
31
+
32
+ async function createEmail(args) {
33
+ const email = args[0];
34
+ if (!email) {
35
+ console.error(JSON.stringify({ status: "error", message: "Missing email. Usage: run402 billing create-email <email>" }));
36
+ process.exit(1);
37
+ }
38
+ const res = await fetch(`${API}/billing/v1/accounts`, {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({ email }),
42
+ });
43
+ const data = await res.json();
44
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
45
+ console.log(JSON.stringify(data, null, 2));
46
+ }
47
+
48
+ async function linkWallet(args) {
49
+ const accountId = args[0];
50
+ const wallet = args[1];
51
+ if (!accountId || !wallet) {
52
+ console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing link-wallet <account_id> <wallet>" }));
53
+ process.exit(1);
54
+ }
55
+ const res = await fetch(`${API}/billing/v1/accounts/${encodeURIComponent(accountId)}/link-wallet`, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify({ wallet }),
59
+ });
60
+ const data = await res.json();
61
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
62
+ console.log(JSON.stringify(data, null, 2));
63
+ }
64
+
65
+ async function tierCheckout(args) {
66
+ const tier = args[0];
67
+ if (!tier) {
68
+ console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing tier-checkout <tier> [--email <e> | --wallet <w>]" }));
69
+ process.exit(1);
70
+ }
71
+ const email = parseFlag(args, "--email");
72
+ const wallet = parseFlag(args, "--wallet");
73
+ if (!email && !wallet) {
74
+ console.error(JSON.stringify({ status: "error", message: "Must provide --email or --wallet" }));
75
+ process.exit(1);
76
+ }
77
+ const body = email ? { email } : { wallet };
78
+ const res = await fetch(`${API}/billing/v1/tiers/${encodeURIComponent(tier)}/checkout`, {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify(body),
82
+ });
83
+ const data = await res.json();
84
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
85
+ console.log(JSON.stringify(data, null, 2));
86
+ }
87
+
88
+ async function buyPack(args) {
89
+ const email = parseFlag(args, "--email");
90
+ const wallet = parseFlag(args, "--wallet");
91
+ if (!email && !wallet) {
92
+ console.error(JSON.stringify({ status: "error", message: "Must provide --email or --wallet" }));
93
+ process.exit(1);
94
+ }
95
+ const body = email ? { email } : { wallet };
96
+ const res = await fetch(`${API}/billing/v1/email-packs/checkout`, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify(body),
100
+ });
101
+ const data = await res.json();
102
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
103
+ console.log(JSON.stringify(data, null, 2));
104
+ }
105
+
106
+ async function autoRecharge(args) {
107
+ const accountId = args[0];
108
+ const state = args[1];
109
+ if (!accountId || !state || !["on", "off"].includes(state)) {
110
+ console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing auto-recharge <account_id> <on|off> [--threshold <n>]" }));
111
+ process.exit(1);
112
+ }
113
+ const thresholdStr = parseFlag(args, "--threshold");
114
+ const body = {
115
+ billing_account_id: accountId,
116
+ enabled: state === "on",
117
+ };
118
+ if (thresholdStr) body.threshold = Number(thresholdStr);
119
+ const res = await fetch(`${API}/billing/v1/email-packs/auto-recharge`, {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify(body),
123
+ });
124
+ const data = await res.json();
125
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
126
+ console.log(JSON.stringify(data, null, 2));
127
+ }
128
+
129
+ async function balance(args) {
130
+ const id = args[0];
131
+ if (!id) {
132
+ console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing balance <email-or-wallet>" }));
133
+ process.exit(1);
134
+ }
135
+ const res = await fetch(`${API}/billing/v1/accounts/${encodeURIComponent(id)}`);
136
+ const data = await res.json();
137
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
138
+ console.log(JSON.stringify(data, null, 2));
139
+ }
140
+
141
+ async function history(args) {
142
+ const id = args[0];
143
+ if (!id) {
144
+ console.error(JSON.stringify({ status: "error", message: "Usage: run402 billing history <email-or-wallet> [--limit <n>]" }));
145
+ process.exit(1);
146
+ }
147
+ const limit = parseFlag(args, "--limit") || "50";
148
+ const res = await fetch(`${API}/billing/v1/accounts/${encodeURIComponent(id)}/history?limit=${encodeURIComponent(limit)}`);
149
+ const data = await res.json();
150
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
151
+ console.log(JSON.stringify(data, null, 2));
152
+ }
153
+
154
+ export async function run(sub, args) {
155
+ if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
156
+ switch (sub) {
157
+ case "create-email": await createEmail(args); break;
158
+ case "link-wallet": await linkWallet(args); break;
159
+ case "tier-checkout": await tierCheckout(args); break;
160
+ case "buy-pack": await buyPack(args); break;
161
+ case "auto-recharge": await autoRecharge(args); break;
162
+ case "balance": await balance(args); break;
163
+ case "history": await history(args); break;
164
+ default:
165
+ console.error(`Unknown subcommand: ${sub}\n`);
166
+ console.log(HELP);
167
+ process.exit(1);
168
+ }
169
+ }
@@ -0,0 +1,262 @@
1
+ import { findProject, API } from "./config.mjs";
2
+
3
+ const HELP = `run402 contracts — KMS-backed Ethereum wallets for smart-contract calls
4
+
5
+ Pricing: $0.04/day per wallet ($1.20/month) plus $0.000005 per contract call.
6
+ Wallet creation requires $1.20 in cash credit (30 days of rent).
7
+ Non-custodial: see https://run402.com/humans/terms.html#non-custodial-kms-wallets
8
+
9
+ Usage:
10
+ run402 contracts <subcommand> [args...]
11
+
12
+ Subcommands:
13
+ provision-wallet <project_id> --chain <base-mainnet|base-sepolia> [--recovery 0x...]
14
+ Provision a KMS wallet ($0.04/day, requires $1.20 prepay).
15
+ get-wallet <project_id> <wallet_id>
16
+ Get wallet metadata + live native balance.
17
+ list-wallets <project_id>
18
+ List all KMS wallets for the project (includes deleted).
19
+ set-recovery <project_id> <wallet_id> [--address 0x... | --clear]
20
+ Set/clear the optional recovery address.
21
+ set-alert <project_id> <wallet_id> --threshold-wei <n>
22
+ Set the low-balance alert threshold (in wei).
23
+ call <project_id> <wallet_id> --to 0x... --abi <json> --fn <name> --args <json> [--value-wei <n>] [--idempotency-key <k>]
24
+ Submit a contract write call (chain gas + $0.000005 KMS sign fee).
25
+ read --chain <chain> --to 0x... --abi <json> --fn <name> --args <json>
26
+ Read-only contract call (free).
27
+ status <project_id> <call_id>
28
+ Get call status, gas used, gas cost USD-micros, receipt.
29
+ drain <project_id> <wallet_id> --to 0x... --confirm
30
+ Drain native balance to a destination address. Works on suspended wallets.
31
+ delete <project_id> <wallet_id> --confirm
32
+ Schedule the KMS key for deletion (refused if balance >= dust).
33
+
34
+ Examples:
35
+ run402 contracts provision-wallet proj_abc --chain base-mainnet
36
+ run402 contracts call proj_abc cwlt_xyz --to 0x1234... --abi '[{"type":"function","name":"ping","inputs":[],"outputs":[]}]' --fn ping --args '[]'
37
+ `;
38
+
39
+ function parseFlag(args, flag) {
40
+ for (let i = 0; i < args.length; i++) {
41
+ if (args[i] === flag && args[i + 1]) return args[i + 1];
42
+ }
43
+ return null;
44
+ }
45
+ function hasFlag(args, flag) {
46
+ return args.includes(flag);
47
+ }
48
+
49
+ async function provisionWallet(projectId, args) {
50
+ const p = findProject(projectId);
51
+ const chain = parseFlag(args, "--chain");
52
+ if (!chain) {
53
+ console.error(JSON.stringify({ status: "error", message: "Missing --chain (base-mainnet or base-sepolia)" }));
54
+ process.exit(1);
55
+ }
56
+ const recovery = parseFlag(args, "--recovery");
57
+ // Soft default of one wallet — confirm if project already has one
58
+ try {
59
+ const listRes = await fetch(`${API}/contracts/v1/wallets`, {
60
+ headers: { Authorization: `Bearer ${p.service_key}` },
61
+ });
62
+ if (listRes.ok) {
63
+ const list = await listRes.json();
64
+ const active = (list.wallets || []).filter((w) => w.status === "active");
65
+ if (active.length >= 1 && !hasFlag(args, "--yes")) {
66
+ console.error(`This project already has ${active.length} active wallet(s). Adding another costs $0.04/day each ($1.20/month). Re-run with --yes to confirm.`);
67
+ process.exit(1);
68
+ }
69
+ }
70
+ } catch { /* best-effort */ }
71
+ const body = { chain };
72
+ if (recovery) body.recovery_address = recovery;
73
+ const res = await fetch(`${API}/contracts/v1/wallets`, {
74
+ method: "POST",
75
+ headers: { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" },
76
+ body: JSON.stringify(body),
77
+ });
78
+ const data = await res.json();
79
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
80
+ console.log(JSON.stringify(data, null, 2));
81
+ }
82
+
83
+ async function getWallet(projectId, walletId) {
84
+ const p = findProject(projectId);
85
+ const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}`, {
86
+ headers: { Authorization: `Bearer ${p.service_key}` },
87
+ });
88
+ const data = await res.json();
89
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
90
+ console.log(JSON.stringify(data, null, 2));
91
+ }
92
+
93
+ async function listWallets(projectId) {
94
+ const p = findProject(projectId);
95
+ const res = await fetch(`${API}/contracts/v1/wallets`, {
96
+ headers: { Authorization: `Bearer ${p.service_key}` },
97
+ });
98
+ const data = await res.json();
99
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
100
+ console.log(JSON.stringify(data, null, 2));
101
+ }
102
+
103
+ async function setRecovery(projectId, walletId, args) {
104
+ const p = findProject(projectId);
105
+ const clear = hasFlag(args, "--clear");
106
+ const address = parseFlag(args, "--address");
107
+ if (!clear && !address) {
108
+ console.error(JSON.stringify({ status: "error", message: "Provide --address 0x... or --clear" }));
109
+ process.exit(1);
110
+ }
111
+ const body = { recovery_address: clear ? null : address };
112
+ const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}/recovery-address`, {
113
+ method: "POST",
114
+ headers: { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" },
115
+ body: JSON.stringify(body),
116
+ });
117
+ const data = await res.json();
118
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
119
+ console.log(JSON.stringify(data, null, 2));
120
+ }
121
+
122
+ async function setAlert(projectId, walletId, args) {
123
+ const p = findProject(projectId);
124
+ const threshold = parseFlag(args, "--threshold-wei");
125
+ if (!threshold) {
126
+ console.error(JSON.stringify({ status: "error", message: "Missing --threshold-wei <n>" }));
127
+ process.exit(1);
128
+ }
129
+ const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}/alert`, {
130
+ method: "POST",
131
+ headers: { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" },
132
+ body: JSON.stringify({ threshold_wei: threshold }),
133
+ });
134
+ const data = await res.json();
135
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
136
+ console.log(JSON.stringify(data, null, 2));
137
+ }
138
+
139
+ async function call(projectId, walletId, args) {
140
+ const p = findProject(projectId);
141
+ const to = parseFlag(args, "--to");
142
+ const abi = parseFlag(args, "--abi");
143
+ const fn = parseFlag(args, "--fn");
144
+ const argsJson = parseFlag(args, "--args");
145
+ const value = parseFlag(args, "--value-wei");
146
+ const chain = parseFlag(args, "--chain") || "base-mainnet";
147
+ const idempotency = parseFlag(args, "--idempotency-key");
148
+ if (!to || !abi || !fn || !argsJson) {
149
+ console.error(JSON.stringify({ status: "error", message: "Required flags: --to, --abi, --fn, --args. Cost: chain gas + $0.000005 KMS sign fee." }));
150
+ process.exit(1);
151
+ }
152
+ const body = {
153
+ wallet_id: walletId,
154
+ chain,
155
+ contract_address: to,
156
+ abi_fragment: JSON.parse(abi),
157
+ function_name: fn,
158
+ args: JSON.parse(argsJson),
159
+ };
160
+ if (value) body.value = value;
161
+ const headers = { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" };
162
+ if (idempotency) headers["Idempotency-Key"] = idempotency;
163
+ const res = await fetch(`${API}/contracts/v1/call`, { method: "POST", headers, body: JSON.stringify(body) });
164
+ const data = await res.json();
165
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
166
+ console.log(JSON.stringify(data, null, 2));
167
+ }
168
+
169
+ async function read(args) {
170
+ const chain = parseFlag(args, "--chain");
171
+ const to = parseFlag(args, "--to");
172
+ const abi = parseFlag(args, "--abi");
173
+ const fn = parseFlag(args, "--fn");
174
+ const argsJson = parseFlag(args, "--args");
175
+ if (!chain || !to || !abi || !fn || !argsJson) {
176
+ console.error(JSON.stringify({ status: "error", message: "Required flags: --chain, --to, --abi, --fn, --args" }));
177
+ process.exit(1);
178
+ }
179
+ const res = await fetch(`${API}/contracts/v1/read`, {
180
+ method: "POST",
181
+ headers: { "Content-Type": "application/json" },
182
+ body: JSON.stringify({
183
+ chain,
184
+ contract_address: to,
185
+ abi_fragment: JSON.parse(abi),
186
+ function_name: fn,
187
+ args: JSON.parse(argsJson),
188
+ }),
189
+ });
190
+ const data = await res.json();
191
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
192
+ console.log(JSON.stringify(data, null, 2));
193
+ }
194
+
195
+ async function status(projectId, callId) {
196
+ const p = findProject(projectId);
197
+ const res = await fetch(`${API}/contracts/v1/calls/${encodeURIComponent(callId)}`, {
198
+ headers: { Authorization: `Bearer ${p.service_key}` },
199
+ });
200
+ const data = await res.json();
201
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
202
+ console.log(JSON.stringify(data, null, 2));
203
+ }
204
+
205
+ async function drain(projectId, walletId, args) {
206
+ const p = findProject(projectId);
207
+ const to = parseFlag(args, "--to");
208
+ if (!to || !hasFlag(args, "--confirm")) {
209
+ console.error(JSON.stringify({ status: "error", message: "Required: --to 0x... and --confirm. Cost: chain gas + $0.000005 KMS sign fee." }));
210
+ process.exit(1);
211
+ }
212
+ const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}/drain`, {
213
+ method: "POST",
214
+ headers: {
215
+ Authorization: `Bearer ${p.service_key}`,
216
+ "Content-Type": "application/json",
217
+ "X-Confirm-Drain": walletId,
218
+ },
219
+ body: JSON.stringify({ destination_address: to }),
220
+ });
221
+ const data = await res.json();
222
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
223
+ console.log(JSON.stringify(data, null, 2));
224
+ }
225
+
226
+ async function deleteWallet(projectId, walletId, args) {
227
+ const p = findProject(projectId);
228
+ if (!hasFlag(args, "--confirm")) {
229
+ console.error(JSON.stringify({ status: "error", message: "Required: --confirm" }));
230
+ process.exit(1);
231
+ }
232
+ const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}`, {
233
+ method: "DELETE",
234
+ headers: {
235
+ Authorization: `Bearer ${p.service_key}`,
236
+ "X-Confirm-Delete": walletId,
237
+ },
238
+ });
239
+ const data = await res.json().catch(() => ({}));
240
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
241
+ console.log(JSON.stringify(data, null, 2));
242
+ }
243
+
244
+ export async function run(sub, args) {
245
+ if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
246
+ switch (sub) {
247
+ case "provision-wallet": await provisionWallet(args[0], args.slice(1)); break;
248
+ case "get-wallet": await getWallet(args[0], args[1]); break;
249
+ case "list-wallets": await listWallets(args[0]); break;
250
+ case "set-recovery": await setRecovery(args[0], args[1], args.slice(2)); break;
251
+ case "set-alert": await setAlert(args[0], args[1], args.slice(2)); break;
252
+ case "call": await call(args[0], args[1], args.slice(2)); break;
253
+ case "read": await read(args); break;
254
+ case "status": await status(args[0], args[1]); break;
255
+ case "drain": await drain(args[0], args[1], args.slice(2)); break;
256
+ case "delete": await deleteWallet(args[0], args[1], args.slice(2)); break;
257
+ default:
258
+ console.error(`Unknown subcommand: ${sub}\n`);
259
+ console.log(HELP);
260
+ process.exit(1);
261
+ }
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.27.0",
3
+ "version": "1.29.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {