run402 1.2.0 → 1.4.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
@@ -13,10 +13,18 @@ Usage:
13
13
  run402 <command> [subcommand] [options]
14
14
 
15
15
  Commands:
16
- wallet Manage your x402 wallet (create, fund, check status)
17
- projects Manage deployed projects (list, query, inspect, renew, delete)
18
- deploy Deploy a full-stack app or static site (Postgres + hosting)
19
- image Generate AI images via x402 micropayments
16
+ wallet Manage your x402 wallet (create, fund, balance, status)
17
+ projects Manage projects (provision, list, query, inspect, renew, delete)
18
+ deploy Deploy a full-stack app or static site (Postgres + hosting)
19
+ functions Manage serverless functions (deploy, invoke, logs, list, delete)
20
+ secrets Manage project secrets (set, list, delete)
21
+ storage Manage file storage (upload, download, list, delete)
22
+ sites Deploy static sites
23
+ subdomains Manage custom subdomains (claim, list, delete)
24
+ apps Browse and manage the app marketplace
25
+ image Generate AI images via x402 micropayments
26
+ message Send messages to Run402 developers
27
+ agent Manage agent identity (contact info)
20
28
 
21
29
  Run 'run402 <command> --help' for detailed usage of each command.
22
30
 
@@ -26,6 +34,8 @@ Examples:
26
34
  run402 deploy --tier prototype --manifest app.json
27
35
  run402 projects list
28
36
  run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
37
+ run402 functions deploy <project_id> my-fn --code handler.ts
38
+ run402 secrets set <project_id> API_KEY sk-1234
29
39
  run402 image generate "a startup mascot, pixel art" --output logo.png
30
40
 
31
41
  Getting started:
@@ -55,11 +65,51 @@ switch (cmd) {
55
65
  await run([sub, ...rest].filter(Boolean));
56
66
  break;
57
67
  }
68
+ case "functions": {
69
+ const { run } = await import("./lib/functions.mjs");
70
+ await run(sub, rest);
71
+ break;
72
+ }
73
+ case "secrets": {
74
+ const { run } = await import("./lib/secrets.mjs");
75
+ await run(sub, rest);
76
+ break;
77
+ }
78
+ case "storage": {
79
+ const { run } = await import("./lib/storage.mjs");
80
+ await run(sub, rest);
81
+ break;
82
+ }
83
+ case "sites": {
84
+ const { run } = await import("./lib/sites.mjs");
85
+ await run(sub, rest);
86
+ break;
87
+ }
88
+ case "subdomains": {
89
+ const { run } = await import("./lib/subdomains.mjs");
90
+ await run(sub, rest);
91
+ break;
92
+ }
93
+ case "apps": {
94
+ const { run } = await import("./lib/apps.mjs");
95
+ await run(sub, rest);
96
+ break;
97
+ }
58
98
  case "image": {
59
99
  const { run } = await import("./lib/image.mjs");
60
100
  await run(sub, rest);
61
101
  break;
62
102
  }
103
+ case "message": {
104
+ const { run } = await import("./lib/message.mjs");
105
+ await run(sub, rest);
106
+ break;
107
+ }
108
+ case "agent": {
109
+ const { run } = await import("./lib/agent.mjs");
110
+ await run(sub, rest);
111
+ break;
112
+ }
63
113
  default:
64
114
  console.error(`Unknown command: ${cmd}\n`);
65
115
  console.log(HELP);
package/lib/agent.mjs ADDED
@@ -0,0 +1,68 @@
1
+ import { readWallet, API, WALLET_FILE } from "./config.mjs";
2
+ import { existsSync } from "fs";
3
+
4
+ const HELP = `run402 agent — Manage agent identity
5
+
6
+ Usage:
7
+ run402 agent contact --name <name> [--email <email>] [--webhook <url>]
8
+
9
+ Notes:
10
+ - Costs $0.001 USDC via x402
11
+ - Registers contact info so Run402 can reach your agent
12
+ - Only name is required; email and webhook are optional
13
+
14
+ Examples:
15
+ run402 agent contact --name my-agent
16
+ run402 agent contact --name my-agent --email ops@example.com --webhook https://example.com/hook
17
+ `;
18
+
19
+ async function contact(args) {
20
+ let name = null, email = null, webhook = null;
21
+ for (let i = 0; i < args.length; i++) {
22
+ if (args[i] === "--name" && args[i + 1]) name = args[++i];
23
+ if (args[i] === "--email" && args[i + 1]) email = args[++i];
24
+ if (args[i] === "--webhook" && args[i + 1]) webhook = args[++i];
25
+ }
26
+ if (!name) { console.error(JSON.stringify({ status: "error", message: "Missing --name <name>" })); process.exit(1); }
27
+ if (!existsSync(WALLET_FILE)) {
28
+ console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
29
+ process.exit(1);
30
+ }
31
+
32
+ const wallet = readWallet();
33
+ const { privateKeyToAccount } = await import("viem/accounts");
34
+ const { createPublicClient, http } = await import("viem");
35
+ const { baseSepolia } = await import("viem/chains");
36
+ const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
37
+ const { ExactEvmScheme } = await import("@x402/evm/exact/client");
38
+ const { toClientEvmSigner } = await import("@x402/evm");
39
+ const account = privateKeyToAccount(wallet.privateKey);
40
+ const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
41
+ const signer = toClientEvmSigner(account, publicClient);
42
+ const client = new x402Client();
43
+ client.register("eip155:84532", new ExactEvmScheme(signer));
44
+ const fetchPaid = wrapFetchWithPayment(fetch, client);
45
+
46
+ const body = { name };
47
+ if (email) body.email = email;
48
+ if (webhook) body.webhook = webhook;
49
+
50
+ const res = await fetchPaid(`${API}/v1/agent/contact`, {
51
+ method: "PUT",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify(body),
54
+ });
55
+ const data = await res.json();
56
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
57
+ console.log(JSON.stringify(data, null, 2));
58
+ }
59
+
60
+ export async function run(sub, args) {
61
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
62
+ if (sub !== "contact") {
63
+ console.error(`Unknown subcommand: ${sub}\n`);
64
+ console.log(HELP);
65
+ process.exit(1);
66
+ }
67
+ await contact(args);
68
+ }
package/lib/apps.mjs ADDED
@@ -0,0 +1,190 @@
1
+ import { existsSync } from "fs";
2
+ import { findProject, readWallet, loadProjects, saveProjects, API, WALLET_FILE, PROJECTS_FILE } from "./config.mjs";
3
+ import { mkdirSync, writeFileSync } from "fs";
4
+
5
+ const HELP = `run402 apps — Browse and manage the app marketplace
6
+
7
+ Usage:
8
+ run402 apps <subcommand> [args...]
9
+
10
+ Subcommands:
11
+ browse [--tag <tag>] Browse public apps
12
+ fork <version_id> <name> [--tier <tier>] [--subdomain <name>]
13
+ Fork a published app into your own project
14
+ publish <id> [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed]
15
+ Publish a project as an app
16
+ versions <id> List published versions of a project
17
+ inspect <version_id> Inspect a published app version
18
+ update <project_id> <version_id> [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed] [--no-fork]
19
+ Update a published version
20
+ delete <project_id> <version_id> Delete a published version
21
+
22
+ Examples:
23
+ run402 apps browse
24
+ run402 apps browse --tag auth
25
+ run402 apps fork ver_abc123 my-todo --tier prototype
26
+ run402 apps publish proj123 --description "Todo app" --tags todo,auth --visibility public --fork-allowed
27
+ run402 apps versions proj123
28
+ run402 apps inspect ver_abc123
29
+ run402 apps update proj123 ver_abc123 --description "Updated" --tags todo
30
+ run402 apps delete proj123 ver_abc123
31
+ `;
32
+
33
+ async function browse(args) {
34
+ let url = `${API}/v1/apps`;
35
+ const tags = [];
36
+ for (let i = 0; i < args.length; i++) {
37
+ if (args[i] === "--tag" && args[i + 1]) tags.push(args[++i]);
38
+ }
39
+ if (tags.length > 0) url += "?" + tags.map(t => `tag=${encodeURIComponent(t)}`).join("&");
40
+ const res = await fetch(url);
41
+ const data = await res.json();
42
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
43
+ console.log(JSON.stringify(data, null, 2));
44
+ }
45
+
46
+ async function fork(versionId, name, args) {
47
+ const opts = { tier: "prototype", subdomain: undefined };
48
+ for (let i = 0; i < args.length; i++) {
49
+ if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
50
+ if (args[i] === "--subdomain" && args[i + 1]) opts.subdomain = args[++i];
51
+ }
52
+ if (!existsSync(WALLET_FILE)) {
53
+ console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
54
+ process.exit(1);
55
+ }
56
+
57
+ const wallet = readWallet();
58
+ const { privateKeyToAccount } = await import("viem/accounts");
59
+ const { createPublicClient, http } = await import("viem");
60
+ const { baseSepolia } = await import("viem/chains");
61
+ const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
62
+ const { ExactEvmScheme } = await import("@x402/evm/exact/client");
63
+ const { toClientEvmSigner } = await import("@x402/evm");
64
+ const account = privateKeyToAccount(wallet.privateKey);
65
+ const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
66
+ const signer = toClientEvmSigner(account, publicClient);
67
+ const client = new x402Client();
68
+ client.register("eip155:84532", new ExactEvmScheme(signer));
69
+ const fetchPaid = wrapFetchWithPayment(fetch, client);
70
+
71
+ const body = { version_id: versionId, name };
72
+ if (opts.subdomain) body.subdomain = opts.subdomain;
73
+
74
+ const res = await fetchPaid(`${API}/v1/fork/${opts.tier}`, {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify(body),
78
+ });
79
+ const data = await res.json();
80
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
81
+
82
+ // Save project credentials locally
83
+ if (data.project_id) {
84
+ const projects = loadProjects();
85
+ projects.push({
86
+ project_id: data.project_id, anon_key: data.anon_key, service_key: data.service_key,
87
+ tier: data.tier, lease_expires_at: data.lease_expires_at,
88
+ site_url: data.site_url || data.subdomain_url, deployed_at: new Date().toISOString(),
89
+ });
90
+ const dir = PROJECTS_FILE.replace(/\/[^/]+$/, "");
91
+ mkdirSync(dir, { recursive: true });
92
+ writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2), { mode: 0o600 });
93
+ }
94
+ console.log(JSON.stringify(data, null, 2));
95
+ }
96
+
97
+ async function publish(projectId, args) {
98
+ const p = findProject(projectId);
99
+ const opts = { description: undefined, tags: undefined, visibility: undefined, forkAllowed: undefined };
100
+ for (let i = 0; i < args.length; i++) {
101
+ if (args[i] === "--description" && args[i + 1]) opts.description = args[++i];
102
+ if (args[i] === "--tags" && args[i + 1]) opts.tags = args[++i].split(",");
103
+ if (args[i] === "--visibility" && args[i + 1]) opts.visibility = args[++i];
104
+ if (args[i] === "--fork-allowed") opts.forkAllowed = true;
105
+ }
106
+ const body = {};
107
+ if (opts.description) body.description = opts.description;
108
+ if (opts.tags) body.tags = opts.tags;
109
+ if (opts.visibility) body.visibility = opts.visibility;
110
+ if (opts.forkAllowed !== undefined) body.fork_allowed = opts.forkAllowed;
111
+
112
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/publish`, {
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 versions(projectId) {
123
+ const p = findProject(projectId);
124
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/versions`, {
125
+ headers: { "Authorization": `Bearer ${p.service_key}` },
126
+ });
127
+ const data = await res.json();
128
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
129
+ console.log(JSON.stringify(data, null, 2));
130
+ }
131
+
132
+ async function inspect(versionId) {
133
+ if (!versionId) { console.error(JSON.stringify({ status: "error", message: "Missing version ID" })); process.exit(1); }
134
+ const res = await fetch(`${API}/v1/apps/${versionId}`);
135
+ const data = await res.json();
136
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
137
+ console.log(JSON.stringify(data, null, 2));
138
+ }
139
+
140
+ async function update(projectId, versionId, args) {
141
+ const p = findProject(projectId);
142
+ const body = {};
143
+ for (let i = 0; i < args.length; i++) {
144
+ if (args[i] === "--description" && args[i + 1]) body.description = args[++i];
145
+ if (args[i] === "--tags" && args[i + 1]) body.tags = args[++i].split(",");
146
+ if (args[i] === "--visibility" && args[i + 1]) body.visibility = args[++i];
147
+ if (args[i] === "--fork-allowed") body.fork_allowed = true;
148
+ if (args[i] === "--no-fork") body.fork_allowed = false;
149
+ }
150
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/versions/${versionId}`, {
151
+ method: "PATCH",
152
+ headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
153
+ body: JSON.stringify(body),
154
+ });
155
+ const data = await res.json();
156
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
157
+ console.log(JSON.stringify(data, null, 2));
158
+ }
159
+
160
+ async function deleteVersion(projectId, versionId) {
161
+ const p = findProject(projectId);
162
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/versions/${versionId}`, {
163
+ method: "DELETE",
164
+ headers: { "Authorization": `Bearer ${p.service_key}` },
165
+ });
166
+ if (res.status === 204 || res.ok) {
167
+ console.log(JSON.stringify({ status: "ok", message: `Version ${versionId} deleted.` }));
168
+ } else {
169
+ const data = await res.json();
170
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
171
+ process.exit(1);
172
+ }
173
+ }
174
+
175
+ export async function run(sub, args) {
176
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
177
+ switch (sub) {
178
+ case "browse": await browse(args); break;
179
+ case "fork": await fork(args[0], args[1], args.slice(2)); break;
180
+ case "publish": await publish(args[0], args.slice(1)); break;
181
+ case "versions": await versions(args[0]); break;
182
+ case "inspect": await inspect(args[0]); break;
183
+ case "update": await update(args[0], args[1], args.slice(2)); break;
184
+ case "delete": await deleteVersion(args[0], args[1]); break;
185
+ default:
186
+ console.error(`Unknown subcommand: ${sub}\n`);
187
+ console.log(HELP);
188
+ process.exit(1);
189
+ }
190
+ }
package/lib/config.mjs CHANGED
@@ -3,9 +3,10 @@
3
3
  * Kept in a separate module so credential reads stay isolated.
4
4
  */
5
5
 
6
- import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
7
- import { join } from "path";
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, renameSync } from "fs";
7
+ import { join, dirname } from "path";
8
8
  import { homedir } from "os";
9
+ import { randomBytes } from "crypto";
9
10
 
10
11
  export const CONFIG_DIR = join(homedir(), ".config", "run402");
11
12
  export const WALLET_FILE = join(CONFIG_DIR, "wallet.json");
@@ -19,8 +20,10 @@ export function readWallet() {
19
20
 
20
21
  export function saveWallet(data) {
21
22
  mkdirSync(CONFIG_DIR, { recursive: true });
22
- writeFileSync(WALLET_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
23
- try { chmodSync(WALLET_FILE, 0o600); } catch {}
23
+ const tmp = join(CONFIG_DIR, `.wallet.${randomBytes(4).toString("hex")}.tmp`);
24
+ writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
25
+ renameSync(tmp, WALLET_FILE);
26
+ chmodSync(WALLET_FILE, 0o600);
24
27
  }
25
28
 
26
29
  export function loadProjects() {
@@ -0,0 +1,150 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { findProject, readWallet, API, WALLET_FILE } from "./config.mjs";
3
+
4
+ const HELP = `run402 functions — Manage serverless functions
5
+
6
+ Usage:
7
+ run402 functions <subcommand> [args...]
8
+
9
+ Subcommands:
10
+ deploy <id> <name> --code <file> [--timeout <s>] [--memory <mb>] [--deps <pkg,...>]
11
+ Deploy a function to a project
12
+ invoke <id> <name> [--method <M>] [--body <json>]
13
+ Invoke a deployed function
14
+ logs <id> <name> [--tail <n>] Get function logs
15
+ list <id> List all functions for a project
16
+ delete <id> <name> Delete a function
17
+
18
+ Examples:
19
+ run402 functions deploy abc123 stripe-webhook --code handler.ts
20
+ run402 functions invoke abc123 stripe-webhook --body '{"event":"test"}'
21
+ run402 functions logs abc123 stripe-webhook --tail 100
22
+ run402 functions list abc123
23
+ run402 functions delete abc123 stripe-webhook
24
+
25
+ Notes:
26
+ - Code must export a default async function: export default async (req: Request) => Response
27
+ - Deploy may require payment if the project lease has expired
28
+ `;
29
+
30
+ async function setupPaidFetch() {
31
+ if (!existsSync(WALLET_FILE)) {
32
+ console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
33
+ process.exit(1);
34
+ }
35
+ const wallet = readWallet();
36
+ const { privateKeyToAccount } = await import("viem/accounts");
37
+ const { createPublicClient, http } = await import("viem");
38
+ const { baseSepolia } = await import("viem/chains");
39
+ const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
40
+ const { ExactEvmScheme } = await import("@x402/evm/exact/client");
41
+ const { toClientEvmSigner } = await import("@x402/evm");
42
+ const account = privateKeyToAccount(wallet.privateKey);
43
+ const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
44
+ const signer = toClientEvmSigner(account, publicClient);
45
+ const client = new x402Client();
46
+ client.register("eip155:84532", new ExactEvmScheme(signer));
47
+ return wrapFetchWithPayment(fetch, client);
48
+ }
49
+
50
+ async function deploy(projectId, name, args) {
51
+ const p = findProject(projectId);
52
+ const opts = { code: null, timeout: undefined, memory: undefined, deps: undefined };
53
+ for (let i = 0; i < args.length; i++) {
54
+ if (args[i] === "--code" && args[i + 1]) opts.code = args[++i];
55
+ if (args[i] === "--timeout" && args[i + 1]) opts.timeout = parseInt(args[++i]);
56
+ if (args[i] === "--memory" && args[i + 1]) opts.memory = parseInt(args[++i]);
57
+ if (args[i] === "--deps" && args[i + 1]) opts.deps = args[++i].split(",");
58
+ }
59
+ if (!opts.code) { console.error(JSON.stringify({ status: "error", message: "Missing --code <file>" })); process.exit(1); }
60
+ const code = readFileSync(opts.code, "utf-8");
61
+ const body = { name, code };
62
+ if (opts.timeout || opts.memory) body.config = {};
63
+ if (opts.timeout) body.config.timeout = opts.timeout;
64
+ if (opts.memory) body.config.memory = opts.memory;
65
+ if (opts.deps) body.deps = opts.deps;
66
+
67
+ const fetchPaid = await setupPaidFetch();
68
+ const res = await fetchPaid(`${API}/admin/v1/projects/${projectId}/functions`, {
69
+ method: "POST",
70
+ headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
71
+ body: JSON.stringify(body),
72
+ });
73
+ const data = await res.json();
74
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
75
+ console.log(JSON.stringify(data, null, 2));
76
+ }
77
+
78
+ async function invoke(projectId, name, args) {
79
+ const p = findProject(projectId);
80
+ const opts = { method: "POST", body: undefined };
81
+ for (let i = 0; i < args.length; i++) {
82
+ if (args[i] === "--method" && args[i + 1]) opts.method = args[++i];
83
+ if (args[i] === "--body" && args[i + 1]) opts.body = args[++i];
84
+ }
85
+ const fetchOpts = {
86
+ method: opts.method,
87
+ headers: { "apikey": p.service_key },
88
+ };
89
+ if (opts.body && opts.method !== "GET" && opts.method !== "HEAD") {
90
+ fetchOpts.headers["Content-Type"] = "application/json";
91
+ fetchOpts.body = opts.body;
92
+ }
93
+ const res = await fetch(`${API}/functions/v1/${name}`, fetchOpts);
94
+ const text = await res.text();
95
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, body: text })); process.exit(1); }
96
+ try { console.log(JSON.stringify(JSON.parse(text), null, 2)); } catch { process.stdout.write(text + "\n"); }
97
+ }
98
+
99
+ async function logs(projectId, name, args) {
100
+ const p = findProject(projectId);
101
+ let tail = 50;
102
+ for (let i = 0; i < args.length; i++) {
103
+ if (args[i] === "--tail" && args[i + 1]) tail = parseInt(args[++i]);
104
+ }
105
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/functions/${encodeURIComponent(name)}/logs?tail=${tail}`, {
106
+ headers: { "Authorization": `Bearer ${p.service_key}` },
107
+ });
108
+ const data = await res.json();
109
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
110
+ console.log(JSON.stringify(data, null, 2));
111
+ }
112
+
113
+ async function list(projectId) {
114
+ const p = findProject(projectId);
115
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/functions`, {
116
+ headers: { "Authorization": `Bearer ${p.service_key}` },
117
+ });
118
+ const data = await res.json();
119
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
120
+ console.log(JSON.stringify(data, null, 2));
121
+ }
122
+
123
+ async function deleteFunction(projectId, name) {
124
+ const p = findProject(projectId);
125
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/functions/${encodeURIComponent(name)}`, {
126
+ method: "DELETE",
127
+ headers: { "Authorization": `Bearer ${p.service_key}` },
128
+ });
129
+ if (res.status === 204 || res.ok) {
130
+ console.log(JSON.stringify({ status: "ok", message: `Function '${name}' deleted.` }));
131
+ } else {
132
+ const data = await res.json().catch(() => ({}));
133
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
134
+ }
135
+ }
136
+
137
+ export async function run(sub, args) {
138
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
139
+ switch (sub) {
140
+ case "deploy": await deploy(args[0], args[1], args.slice(2)); break;
141
+ case "invoke": await invoke(args[0], args[1], args.slice(2)); break;
142
+ case "logs": await logs(args[0], args[1], args.slice(2)); break;
143
+ case "list": await list(args[0]); break;
144
+ case "delete": await deleteFunction(args[0], args[1]); break;
145
+ default:
146
+ console.error(`Unknown subcommand: ${sub}\n`);
147
+ console.log(HELP);
148
+ process.exit(1);
149
+ }
150
+ }
@@ -0,0 +1,56 @@
1
+ import { readWallet, API, WALLET_FILE } from "./config.mjs";
2
+ import { existsSync } from "fs";
3
+
4
+ const HELP = `run402 message — Send messages to Run402 developers
5
+
6
+ Usage:
7
+ run402 message send <text>
8
+
9
+ Notes:
10
+ - Costs $0.01 USDC via x402
11
+ - Requires a funded wallet
12
+
13
+ Examples:
14
+ run402 message send "Hello from my agent!"
15
+ `;
16
+
17
+ async function send(text) {
18
+ if (!text) { console.error(JSON.stringify({ status: "error", message: "Missing message text" })); process.exit(1); }
19
+ if (!existsSync(WALLET_FILE)) {
20
+ console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
21
+ process.exit(1);
22
+ }
23
+
24
+ const wallet = readWallet();
25
+ const { privateKeyToAccount } = await import("viem/accounts");
26
+ const { createPublicClient, http } = await import("viem");
27
+ const { baseSepolia } = await import("viem/chains");
28
+ const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
29
+ const { ExactEvmScheme } = await import("@x402/evm/exact/client");
30
+ const { toClientEvmSigner } = await import("@x402/evm");
31
+ const account = privateKeyToAccount(wallet.privateKey);
32
+ const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
33
+ const signer = toClientEvmSigner(account, publicClient);
34
+ const client = new x402Client();
35
+ client.register("eip155:84532", new ExactEvmScheme(signer));
36
+ const fetchPaid = wrapFetchWithPayment(fetch, client);
37
+
38
+ const res = await fetchPaid(`${API}/v1/message`, {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({ message: text }),
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
+ export async function run(sub, args) {
49
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
50
+ if (sub !== "send") {
51
+ console.error(`Unknown subcommand: ${sub}\n`);
52
+ console.log(HELP);
53
+ process.exit(1);
54
+ }
55
+ await send(args.join(" "));
56
+ }
package/lib/projects.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { findProject, loadProjects, saveProjects, readWallet, API, WALLET_FILE } from "./config.mjs";
2
- import { existsSync } from "fs";
1
+ import { findProject, loadProjects, saveProjects, readWallet, API, WALLET_FILE, PROJECTS_FILE } from "./config.mjs";
2
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
3
3
 
4
4
  const HELP = `run402 projects — Manage your deployed Run402 projects
5
5
 
@@ -7,27 +7,35 @@ Usage:
7
7
  run402 projects <subcommand> [args...]
8
8
 
9
9
  Subcommands:
10
- list List all your projects (IDs, tiers, URLs, expiry)
11
- sql <id> "<query>" Run a SQL query against a project's Postgres DB
12
- rest <id> <table> [params] Query a table via the REST API (PostgREST)
13
- usage <id> Show compute/storage usage for a project
14
- schema <id> Inspect the database schema
15
- renew <id> Extend the project lease (pays via x402)
16
- delete <id> Delete a project and remove it from local state
10
+ quote Show pricing tiers
11
+ provision [--tier <tier>] [--name <n>] Provision a new Postgres project (pays via x402)
12
+ list List all your projects (IDs, tiers, URLs, expiry)
13
+ sql <id> "<query>" Run a SQL query against a project's Postgres DB
14
+ rest <id> <table> [params] Query a table via the REST API (PostgREST)
15
+ usage <id> Show compute/storage usage for a project
16
+ schema <id> Inspect the database schema
17
+ rls <id> <template> <tables_json> Apply Row-Level Security policies
18
+ renew <id> Extend the project lease (pays via x402)
19
+ delete <id> Delete a project and remove it from local state
17
20
 
18
21
  Examples:
22
+ run402 projects quote
23
+ run402 projects provision --tier prototype
24
+ run402 projects provision --tier hobby --name my-app
19
25
  run402 projects list
20
26
  run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
21
27
  run402 projects rest abc123 users "limit=10&select=id,name"
22
28
  run402 projects usage abc123
23
29
  run402 projects schema abc123
30
+ run402 projects rls abc123 public_read '[{"table":"posts"}]'
24
31
  run402 projects renew abc123
25
32
  run402 projects delete abc123
26
33
 
27
34
  Notes:
28
35
  - <id> is the project_id shown in 'run402 projects list'
29
36
  - 'rest' uses PostgREST query syntax (table name + optional query string)
30
- - 'renew' requires a funded wallet — payment is automatic via x402
37
+ - 'renew' and 'provision' require a funded wallet — payment is automatic via x402
38
+ - RLS templates: user_owns_rows, public_read, public_read_write
31
39
  `;
32
40
 
33
41
  async function setupPaidFetch() {
@@ -50,6 +58,56 @@ async function setupPaidFetch() {
50
58
  return wrapFetchWithPayment(fetch, client);
51
59
  }
52
60
 
61
+ async function quote() {
62
+ const res = await fetch(`${API}/v1/projects`);
63
+ const data = await res.json();
64
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
65
+ console.log(JSON.stringify(data, null, 2));
66
+ }
67
+
68
+ async function provision(args) {
69
+ const opts = { tier: "prototype", name: undefined };
70
+ for (let i = 0; i < args.length; i++) {
71
+ if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
72
+ if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
73
+ }
74
+ const fetchPaid = await setupPaidFetch();
75
+ const body = { tier: opts.tier };
76
+ if (opts.name) body.name = opts.name;
77
+ const res = await fetchPaid(`${API}/v1/projects`, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify(body),
81
+ });
82
+ const data = await res.json();
83
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
84
+ // Save project credentials locally
85
+ if (data.project_id) {
86
+ const projects = loadProjects();
87
+ projects.push({
88
+ project_id: data.project_id, anon_key: data.anon_key, service_key: data.service_key,
89
+ tier: data.tier, lease_expires_at: data.lease_expires_at, deployed_at: new Date().toISOString(),
90
+ });
91
+ const dir = PROJECTS_FILE.replace(/\/[^/]+$/, "");
92
+ mkdirSync(dir, { recursive: true });
93
+ writeFileSync(PROJECTS_FILE, JSON.stringify(projects, null, 2), { mode: 0o600 });
94
+ }
95
+ console.log(JSON.stringify(data, null, 2));
96
+ }
97
+
98
+ async function rls(projectId, template, tablesJson) {
99
+ const p = findProject(projectId);
100
+ const tables = JSON.parse(tablesJson);
101
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/rls`, {
102
+ method: "POST",
103
+ headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
104
+ body: JSON.stringify({ template, tables }),
105
+ });
106
+ const data = await res.json();
107
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
108
+ console.log(JSON.stringify(data, null, 2));
109
+ }
110
+
53
111
  async function list() {
54
112
  const projects = loadProjects();
55
113
  if (projects.length === 0) { console.log(JSON.stringify({ status: "ok", projects: [], message: "No projects yet." })); return; }
@@ -113,13 +171,16 @@ export async function run(sub, args) {
113
171
  process.exit(0);
114
172
  }
115
173
  switch (sub) {
116
- case "list": await list(); break;
117
- case "sql": await sqlCmd(args[0], args[1]); break;
118
- case "rest": await rest(args[0], args[1], args[2]); break;
119
- case "usage": await usage(args[0]); break;
120
- case "schema": await schema(args[0]); break;
121
- case "renew": await renew(args[0]); break;
122
- case "delete": await deleteProject(args[0]); break;
174
+ case "quote": await quote(); break;
175
+ case "provision": await provision(args); break;
176
+ case "list": await list(); break;
177
+ case "sql": await sqlCmd(args[0], args[1]); break;
178
+ case "rest": await rest(args[0], args[1], args[2]); break;
179
+ case "usage": await usage(args[0]); break;
180
+ case "schema": await schema(args[0]); break;
181
+ case "rls": await rls(args[0], args[1], args[2]); break;
182
+ case "renew": await renew(args[0]); break;
183
+ case "delete": await deleteProject(args[0]); break;
123
184
  default:
124
185
  console.error(`Unknown subcommand: ${sub}\n`);
125
186
  console.log(HELP);
@@ -0,0 +1,70 @@
1
+ import { findProject, API } from "./config.mjs";
2
+
3
+ const HELP = `run402 secrets — Manage project secrets
4
+
5
+ Usage:
6
+ run402 secrets <subcommand> [args...]
7
+
8
+ Subcommands:
9
+ set <id> <key> <value> Set a secret on a project
10
+ list <id> List all secrets for a project
11
+ delete <id> <key> Delete a secret from a project
12
+
13
+ Examples:
14
+ run402 secrets set abc123 STRIPE_KEY sk-1234
15
+ run402 secrets list abc123
16
+ run402 secrets delete abc123 STRIPE_KEY
17
+
18
+ Notes:
19
+ - Secrets are injected as process.env in serverless functions
20
+ - Values are never shown after being set
21
+ `;
22
+
23
+ async function set(projectId, key, value) {
24
+ const p = findProject(projectId);
25
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/secrets`, {
26
+ method: "POST",
27
+ headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
28
+ body: JSON.stringify({ key, value }),
29
+ });
30
+ const data = await res.json();
31
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
32
+ console.log(JSON.stringify({ status: "ok", message: `Secret '${key}' set for project ${projectId}.` }));
33
+ }
34
+
35
+ async function list(projectId) {
36
+ const p = findProject(projectId);
37
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/secrets`, {
38
+ headers: { "Authorization": `Bearer ${p.service_key}` },
39
+ });
40
+ const data = await res.json();
41
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
42
+ console.log(JSON.stringify(data, null, 2));
43
+ }
44
+
45
+ async function deleteSecret(projectId, key) {
46
+ const p = findProject(projectId);
47
+ const res = await fetch(`${API}/admin/v1/projects/${projectId}/secrets/${encodeURIComponent(key)}`, {
48
+ method: "DELETE",
49
+ headers: { "Authorization": `Bearer ${p.service_key}` },
50
+ });
51
+ if (res.status === 204 || res.ok) {
52
+ console.log(JSON.stringify({ status: "ok", message: `Secret '${key}' deleted.` }));
53
+ } else {
54
+ const data = await res.json().catch(() => ({}));
55
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
56
+ }
57
+ }
58
+
59
+ export async function run(sub, args) {
60
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
61
+ switch (sub) {
62
+ case "set": await set(args[0], args[1], args[2]); break;
63
+ case "list": await list(args[0]); break;
64
+ case "delete": await deleteSecret(args[0], args[1]); break;
65
+ default:
66
+ console.error(`Unknown subcommand: ${sub}\n`);
67
+ console.log(HELP);
68
+ process.exit(1);
69
+ }
70
+ }
package/lib/sites.mjs ADDED
@@ -0,0 +1,112 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { readWallet, API, WALLET_FILE } from "./config.mjs";
3
+
4
+ const HELP = `run402 sites — Deploy and manage static sites
5
+
6
+ Usage:
7
+ run402 sites deploy --name <name> --manifest <file> [--project <id>] [--target <target>]
8
+ run402 sites status <deployment_id>
9
+ cat manifest.json | run402 sites deploy --name <name>
10
+
11
+ Subcommands:
12
+ deploy Deploy a static site
13
+ status Check the status of a deployment
14
+
15
+ Options (deploy):
16
+ --name <name> Site name (e.g. 'portfolio', 'family-todo')
17
+ --manifest <file> Path to manifest JSON file (or read from stdin)
18
+ --project <id> Optional project ID to link this deployment to
19
+ --target <target> Deployment target (e.g. 'production')
20
+ --help, -h Show this help message
21
+
22
+ Manifest format (JSON):
23
+ {
24
+ "files": [
25
+ { "file": "index.html", "data": "<html>...</html>" },
26
+ { "file": "style.css", "data": "body { margin: 0; }" }
27
+ ]
28
+ }
29
+
30
+ Examples:
31
+ run402 sites deploy --name my-site --manifest site.json
32
+ run402 sites status dep_abc123
33
+ cat site.json | run402 sites deploy --name my-site
34
+
35
+ Notes:
36
+ - Must include at least index.html in the files array
37
+ - Requires a funded wallet — payment ($0.05 USDC) is automatic via x402
38
+ `;
39
+
40
+ async function readStdin() {
41
+ const chunks = [];
42
+ for await (const chunk of process.stdin) chunks.push(chunk);
43
+ return Buffer.concat(chunks).toString("utf-8");
44
+ }
45
+
46
+ async function deploy(args) {
47
+ const opts = { name: null, manifest: null, project: undefined, target: undefined };
48
+ for (let i = 0; i < args.length; i++) {
49
+ if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
50
+ if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
51
+ if (args[i] === "--manifest" && args[i + 1]) opts.manifest = args[++i];
52
+ if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
53
+ if (args[i] === "--target" && args[i + 1]) opts.target = args[++i];
54
+ }
55
+ if (!opts.name) { console.error(JSON.stringify({ status: "error", message: "Missing --name <name>" })); process.exit(1); }
56
+ if (!existsSync(WALLET_FILE)) {
57
+ console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
58
+ process.exit(1);
59
+ }
60
+
61
+ const manifest = opts.manifest ? JSON.parse(readFileSync(opts.manifest, "utf-8")) : JSON.parse(await readStdin());
62
+ const body = { name: opts.name, files: manifest.files };
63
+ if (opts.project) body.project = opts.project;
64
+ if (opts.target) body.target = opts.target;
65
+
66
+ const wallet = readWallet();
67
+ const { privateKeyToAccount } = await import("viem/accounts");
68
+ const { createPublicClient, http } = await import("viem");
69
+ const { baseSepolia } = await import("viem/chains");
70
+ const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
71
+ const { ExactEvmScheme } = await import("@x402/evm/exact/client");
72
+ const { toClientEvmSigner } = await import("@x402/evm");
73
+ const account = privateKeyToAccount(wallet.privateKey);
74
+ const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
75
+ const signer = toClientEvmSigner(account, publicClient);
76
+ const client = new x402Client();
77
+ client.register("eip155:84532", new ExactEvmScheme(signer));
78
+ const fetchPaid = wrapFetchWithPayment(fetch, client);
79
+
80
+ const res = await fetchPaid(`${API}/v1/deployments`, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify(body),
84
+ });
85
+ const data = await res.json();
86
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
87
+ console.log(JSON.stringify(data, null, 2));
88
+ }
89
+
90
+ async function status(args) {
91
+ let deploymentId = null;
92
+ for (let i = 0; i < args.length; i++) {
93
+ if (!args[i].startsWith("-")) { deploymentId = args[i]; break; }
94
+ }
95
+ if (!deploymentId) { console.error(JSON.stringify({ status: "error", message: "Missing deployment ID" })); process.exit(1); }
96
+ const res = await fetch(`${API}/v1/deployments/${deploymentId}`);
97
+ const data = await res.json();
98
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
99
+ console.log(JSON.stringify(data, null, 2));
100
+ }
101
+
102
+ export async function run(sub, args) {
103
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
104
+ switch (sub) {
105
+ case "deploy": await deploy(args); break;
106
+ case "status": await status(args); break;
107
+ default:
108
+ console.error(`Unknown subcommand: ${sub}\n`);
109
+ console.log(HELP);
110
+ process.exit(1);
111
+ }
112
+ }
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from "fs";
2
+ import { findProject, API } from "./config.mjs";
3
+
4
+ const HELP = `run402 storage — Manage project file storage
5
+
6
+ Usage:
7
+ run402 storage <subcommand> [args...]
8
+
9
+ Subcommands:
10
+ upload <id> <bucket> <path> [--file <local>] [--content-type <mime>]
11
+ Upload a file to storage
12
+ download <id> <bucket> <path> Download a file from storage
13
+ delete <id> <bucket> <path> Delete a file from storage
14
+ list <id> <bucket> List files in a bucket
15
+
16
+ Examples:
17
+ run402 storage upload abc123 assets logo.png --file ./logo.png --content-type image/png
18
+ echo "hello" | run402 storage upload abc123 data notes.txt
19
+ run402 storage download abc123 assets logo.png
20
+ run402 storage list abc123 assets
21
+ run402 storage delete abc123 assets logo.png
22
+
23
+ Notes:
24
+ - <id> is the project_id from 'run402 projects list'
25
+ - Upload reads from --file or stdin if no --file is given
26
+ `;
27
+
28
+ async function readStdin() {
29
+ const chunks = [];
30
+ for await (const chunk of process.stdin) chunks.push(chunk);
31
+ return Buffer.concat(chunks).toString("utf-8");
32
+ }
33
+
34
+ async function upload(projectId, bucket, path, args) {
35
+ const p = findProject(projectId);
36
+ const opts = { file: null, contentType: "text/plain" };
37
+ for (let i = 0; i < args.length; i++) {
38
+ if (args[i] === "--file" && args[i + 1]) opts.file = args[++i];
39
+ if (args[i] === "--content-type" && args[i + 1]) opts.contentType = args[++i];
40
+ }
41
+ const content = opts.file ? readFileSync(opts.file, "utf-8") : await readStdin();
42
+ const res = await fetch(`${API}/storage/v1/object/${bucket}/${path}`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": opts.contentType, "apikey": p.anon_key, "Authorization": `Bearer ${p.anon_key}` },
45
+ body: content,
46
+ });
47
+ const data = await res.json();
48
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
49
+ console.log(JSON.stringify(data, null, 2));
50
+ }
51
+
52
+ async function download(projectId, bucket, path) {
53
+ const p = findProject(projectId);
54
+ const res = await fetch(`${API}/storage/v1/object/${bucket}/${path}`, {
55
+ headers: { "apikey": p.anon_key, "Authorization": `Bearer ${p.anon_key}` },
56
+ });
57
+ if (!res.ok) {
58
+ const data = await res.json().catch(() => ({}));
59
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
60
+ }
61
+ const text = await res.text();
62
+ process.stdout.write(text);
63
+ }
64
+
65
+ async function deleteFile(projectId, bucket, path) {
66
+ const p = findProject(projectId);
67
+ const res = await fetch(`${API}/storage/v1/object/${bucket}/${path}`, {
68
+ method: "DELETE",
69
+ headers: { "apikey": p.anon_key, "Authorization": `Bearer ${p.anon_key}` },
70
+ });
71
+ if (res.status === 204 || res.ok) {
72
+ console.log(JSON.stringify({ status: "ok", message: `File '${bucket}/${path}' deleted.` }));
73
+ } else {
74
+ const data = await res.json().catch(() => ({}));
75
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
76
+ }
77
+ }
78
+
79
+ async function list(projectId, bucket) {
80
+ const p = findProject(projectId);
81
+ const res = await fetch(`${API}/storage/v1/object/list/${bucket}`, {
82
+ headers: { "apikey": p.anon_key, "Authorization": `Bearer ${p.anon_key}` },
83
+ });
84
+ const data = await res.json();
85
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
86
+ console.log(JSON.stringify(data, null, 2));
87
+ }
88
+
89
+ export async function run(sub, args) {
90
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
91
+ switch (sub) {
92
+ case "upload": await upload(args[0], args[1], args[2], args.slice(3)); break;
93
+ case "download": await download(args[0], args[1], args[2]); break;
94
+ case "delete": await deleteFile(args[0], args[1], args[2]); break;
95
+ case "list": await list(args[0], args[1]); break;
96
+ default:
97
+ console.error(`Unknown subcommand: ${sub}\n`);
98
+ console.log(HELP);
99
+ process.exit(1);
100
+ }
101
+ }
@@ -0,0 +1,87 @@
1
+ import { findProject, API } from "./config.mjs";
2
+
3
+ const HELP = `run402 subdomains — Manage custom subdomains
4
+
5
+ Usage:
6
+ run402 subdomains <subcommand> [args...]
7
+
8
+ Subcommands:
9
+ claim <deployment_id> <name> [--project <id>] Claim a subdomain for a deployment
10
+ delete <name> [--project <id>] Release a subdomain
11
+ list <id> List subdomains for a project
12
+
13
+ Examples:
14
+ run402 subdomains claim dpl_abc123 myapp
15
+ run402 subdomains claim dpl_abc123 myapp --project proj123
16
+ run402 subdomains delete myapp
17
+ run402 subdomains list proj123
18
+
19
+ Notes:
20
+ - Subdomain names: 3-63 chars, lowercase alphanumeric + hyphens
21
+ - Creates <name>.run402.com pointing to the deployment
22
+ `;
23
+
24
+ async function claim(deploymentId, name, args) {
25
+ const opts = { project: null };
26
+ for (let i = 0; i < args.length; i++) {
27
+ if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
28
+ }
29
+ const headers = { "Content-Type": "application/json" };
30
+ if (opts.project) {
31
+ const p = findProject(opts.project);
32
+ headers["Authorization"] = `Bearer ${p.service_key}`;
33
+ }
34
+ const res = await fetch(`${API}/v1/subdomains`, {
35
+ method: "POST",
36
+ headers,
37
+ body: JSON.stringify({ name, deployment_id: deploymentId }),
38
+ });
39
+ const data = await res.json();
40
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
41
+ console.log(JSON.stringify(data, null, 2));
42
+ }
43
+
44
+ async function deleteSubdomain(name, args) {
45
+ const opts = { project: null };
46
+ for (let i = 0; i < args.length; i++) {
47
+ if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
48
+ }
49
+ const headers = {};
50
+ if (opts.project) {
51
+ const p = findProject(opts.project);
52
+ headers["Authorization"] = `Bearer ${p.service_key}`;
53
+ }
54
+ const res = await fetch(`${API}/v1/subdomains/${encodeURIComponent(name)}`, {
55
+ method: "DELETE",
56
+ headers,
57
+ });
58
+ if (res.status === 204 || res.ok) {
59
+ console.log(JSON.stringify({ status: "ok", message: `Subdomain '${name}' released.` }));
60
+ } else {
61
+ const data = await res.json().catch(() => ({}));
62
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1);
63
+ }
64
+ }
65
+
66
+ async function list(projectId) {
67
+ const p = findProject(projectId);
68
+ const res = await fetch(`${API}/v1/subdomains`, {
69
+ headers: { "Authorization": `Bearer ${p.service_key}` },
70
+ });
71
+ const data = await res.json();
72
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
73
+ console.log(JSON.stringify(data, null, 2));
74
+ }
75
+
76
+ export async function run(sub, args) {
77
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
78
+ switch (sub) {
79
+ case "claim": await claim(args[0], args[1], args.slice(2)); break;
80
+ case "delete": await deleteSubdomain(args[0], args.slice(1)); break;
81
+ case "list": await list(args[0]); break;
82
+ default:
83
+ console.error(`Unknown subcommand: ${sub}\n`);
84
+ console.log(HELP);
85
+ process.exit(1);
86
+ }
87
+ }
package/lib/wallet.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { readWallet, saveWallet, API } from "./config.mjs";
1
+ import { readWallet, saveWallet, WALLET_FILE, API } from "./config.mjs";
2
2
 
3
3
  const HELP = `run402 wallet — Manage your x402 wallet
4
4
 
@@ -9,25 +9,34 @@ Subcommands:
9
9
  status Show wallet address, network, and funding status
10
10
  create Generate a new wallet and save it locally
11
11
  fund Request test USDC from the Run402 faucet (Base Sepolia)
12
+ balance Show on-chain USDC (mainnet + testnet) and Run402 billing balance
12
13
  export Print the wallet address (useful for scripting)
14
+ checkout Create a billing checkout session (--amount <usd_micros>)
15
+ history View billing transaction history (--limit <n>)
13
16
 
14
17
  Notes:
15
18
  - Wallet is stored locally at ~/.run402/wallet.json
16
- - Network: Base Sepolia (testnet) uses USDC for x402 payments
17
- - You need to create and fund a wallet before deploying or generating images
19
+ - The wallet works on any EVM chain (currently Run402 uses Base Mainnet and Sepolia for testnet)
20
+ - You need to create and fund a wallet before any x402 transaction with Run402
18
21
 
19
22
  Examples:
20
23
  run402 wallet create
21
24
  run402 wallet status
22
25
  run402 wallet fund
23
26
  run402 wallet export
27
+ run402 wallet checkout --amount 5000000
28
+ run402 wallet history --limit 10
24
29
  `;
25
30
 
31
+ const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
32
+ const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
33
+ const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
34
+
26
35
  async function loadDeps() {
27
36
  const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
28
37
  const { createPublicClient, http } = await import("viem");
29
- const { baseSepolia } = await import("viem/chains");
30
- return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, baseSepolia };
38
+ const { base, baseSepolia } = await import("viem/chains");
39
+ return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, base, baseSepolia };
31
40
  }
32
41
 
33
42
  async function status() {
@@ -36,7 +45,7 @@ async function status() {
36
45
  console.log(JSON.stringify({ status: "no_wallet", message: "No wallet found. Run: run402 wallet create" }));
37
46
  return;
38
47
  }
39
- console.log(JSON.stringify({ status: "ok", address: w.address, network: w.network || "base-sepolia", created: w.created, funded: w.funded || false }));
48
+ console.log(JSON.stringify({ status: "ok", address: w.address, created: w.created, funded: w.funded || false, path: WALLET_FILE }));
40
49
  }
41
50
 
42
51
  async function create() {
@@ -47,22 +56,74 @@ async function create() {
47
56
  const { generatePrivateKey, privateKeyToAccount } = await loadDeps();
48
57
  const privateKey = generatePrivateKey();
49
58
  const account = privateKeyToAccount(privateKey);
50
- saveWallet({ address: account.address, privateKey, network: "base-sepolia", created: new Date().toISOString(), funded: false });
51
- console.log(JSON.stringify({ status: "ok", address: account.address, message: "Wallet created and saved." }));
59
+ saveWallet({ address: account.address, privateKey, created: new Date().toISOString(), funded: false });
60
+ console.log(JSON.stringify({ status: "ok", address: account.address, message: `Wallet created. Stored locally at ${WALLET_FILE}` }));
52
61
  }
53
62
 
54
63
  async function fund() {
55
64
  const w = readWallet();
56
65
  if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
66
+
67
+ const { createPublicClient, http, baseSepolia } = await loadDeps();
68
+ const client = createPublicClient({ chain: baseSepolia, transport: http() });
69
+ const before = await readUsdcBalance(client, USDC_SEPOLIA, w.address).catch(() => 0);
70
+
57
71
  const res = await fetch(`${API}/v1/faucet`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address: w.address }) });
58
72
  const data = await res.json();
59
- if (res.ok) {
60
- saveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
61
- console.log(JSON.stringify({ status: "ok", ...data }));
62
- } else {
73
+ if (!res.ok) {
63
74
  console.log(JSON.stringify({ status: "error", ...data }));
64
75
  process.exit(1);
65
76
  }
77
+
78
+ const MAX_WAIT = 30;
79
+ for (let i = 0; i < MAX_WAIT; i++) {
80
+ await new Promise(r => setTimeout(r, 1000));
81
+ const now = await readUsdcBalance(client, USDC_SEPOLIA, w.address).catch(() => before);
82
+ if (now > before) {
83
+ saveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
84
+ console.log(JSON.stringify({
85
+ address: w.address,
86
+ onchain: {
87
+ "base-sepolia_usd_micros": now,
88
+ },
89
+ }, null, 2));
90
+ return;
91
+ }
92
+ }
93
+
94
+ saveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
95
+ console.log(JSON.stringify({ status: "ok", message: "Faucet request sent but balance not yet confirmed", ...data }));
96
+ }
97
+
98
+ async function readUsdcBalance(client, usdc, address) {
99
+ const raw = await client.readContract({ address: usdc, abi: USDC_ABI, functionName: "balanceOf", args: [address] });
100
+ return Number(raw);
101
+ }
102
+
103
+ async function balance() {
104
+ const w = readWallet();
105
+ if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
106
+
107
+ const { createPublicClient, http, base, baseSepolia } = await loadDeps();
108
+ const mainnetClient = createPublicClient({ chain: base, transport: http() });
109
+ const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
110
+
111
+ const [mainnetUsdc, sepoliaUsdc, billingRes] = await Promise.all([
112
+ readUsdcBalance(mainnetClient, USDC_MAINNET, w.address).catch(() => null),
113
+ readUsdcBalance(sepoliaClient, USDC_SEPOLIA, w.address).catch(() => null),
114
+ fetch(`${API}/v1/billing/accounts/${w.address.toLowerCase()}`),
115
+ ]);
116
+
117
+ const billing = billingRes.ok ? await billingRes.json() : null;
118
+
119
+ console.log(JSON.stringify({
120
+ address: w.address,
121
+ onchain: {
122
+ "base-mainnet_usd_micros": mainnetUsdc,
123
+ "base-sepolia_usd_micros": sepoliaUsdc,
124
+ },
125
+ run402: billing ? { balance_usd_micros: billing.available_usd_micros } : "no billing account",
126
+ }, null, 2));
66
127
  }
67
128
 
68
129
  async function exportAddr() {
@@ -71,16 +132,50 @@ async function exportAddr() {
71
132
  console.log(w.address);
72
133
  }
73
134
 
135
+ async function checkout(args) {
136
+ const w = readWallet();
137
+ if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
138
+ let amount = null;
139
+ for (let i = 0; i < args.length; i++) {
140
+ if (args[i] === "--amount" && args[i + 1]) amount = parseInt(args[++i], 10);
141
+ }
142
+ if (!amount) { console.error(JSON.stringify({ status: "error", message: "Missing --amount <usd_micros> (e.g. --amount 5000000 for $5)" })); process.exit(1); }
143
+ const res = await fetch(`${API}/v1/billing/checkouts`, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({ wallet: w.address.toLowerCase(), amount_usd_micros: amount }),
147
+ });
148
+ const data = await res.json();
149
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
150
+ console.log(JSON.stringify(data, null, 2));
151
+ }
152
+
153
+ async function history(args) {
154
+ const w = readWallet();
155
+ if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
156
+ let limit = 20;
157
+ for (let i = 0; i < args.length; i++) {
158
+ if (args[i] === "--limit" && args[i + 1]) limit = parseInt(args[++i], 10);
159
+ }
160
+ const res = await fetch(`${API}/v1/billing/accounts/${w.address.toLowerCase()}/history?limit=${limit}`);
161
+ const data = await res.json();
162
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
163
+ console.log(JSON.stringify(data, null, 2));
164
+ }
165
+
74
166
  export async function run(sub, args) {
75
167
  if (!sub || sub === '--help' || sub === '-h') {
76
168
  console.log(HELP);
77
169
  process.exit(0);
78
170
  }
79
171
  switch (sub) {
80
- case "status": await status(); break;
81
- case "create": await create(); break;
82
- case "fund": await fund(); break;
83
- case "export": await exportAddr(); break;
172
+ case "status": await status(); break;
173
+ case "create": await create(); break;
174
+ case "fund": await fund(); break;
175
+ case "balance": await balance(); break;
176
+ case "export": await exportAddr(); break;
177
+ case "checkout": await checkout(args); break;
178
+ case "history": await history(args); break;
84
179
  default:
85
180
  console.error(`Unknown subcommand: ${sub}\n`);
86
181
  console.log(HELP);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 micropayments.",
5
5
  "type": "module",
6
6
  "bin": {