run402 1.35.0 → 1.35.1

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/lib/init.mjs CHANGED
@@ -12,6 +12,8 @@ const HELP = `run402 init — Set up allowance, funding, and check tier status
12
12
  Usage:
13
13
  run402 init Set up with x402 (Base Sepolia) — default
14
14
  run402 init mpp Set up with MPP (Tempo Moderato)
15
+ run402 init --json Same as init, but emit a JSON summary on stdout
16
+ (human lines go to stderr — for agent automation)
15
17
 
16
18
  Steps (idempotent — safe to re-run):
17
19
  1. Creates config directory (~/.config/run402)
@@ -25,12 +27,28 @@ Run this once to get started, or again to check your setup.
25
27
  `;
26
28
 
27
29
  function short(addr) { return addr.slice(0, 6) + "..." + addr.slice(-4); }
28
- function line(label, value) { console.log(` ${label.padEnd(10)} ${value}`); }
29
30
 
30
31
  export async function run(args = []) {
31
32
  if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
33
+ const jsonMode = args.includes("--json");
32
34
  const isMpp = args[0] === "mpp";
33
- console.log();
35
+
36
+ // In --json mode, human-readable lines go to stderr so stdout stays clean for
37
+ // agents. We also collect structured data for the final JSON emit.
38
+ const write = jsonMode ? (s) => console.error(s) : (s) => console.log(s);
39
+ const line = (label, value) => write(` ${label.padEnd(10)} ${value}`);
40
+ const summary = {
41
+ config_dir: CONFIG_DIR,
42
+ allowance: null,
43
+ rail: null,
44
+ network: null,
45
+ balance: null,
46
+ tier: null,
47
+ projects_saved: 0,
48
+ next_step: null,
49
+ };
50
+
51
+ write("");
34
52
 
35
53
  // 1. Config directory
36
54
  mkdirSync(CONFIG_DIR, { recursive: true });
@@ -58,6 +76,10 @@ export async function run(args = []) {
58
76
  line("Allowance", short(allowance.address));
59
77
  }
60
78
 
79
+ summary.allowance = { address: allowance.address, funded: allowance.funded || false };
80
+ summary.network = isMpp ? "tempo-moderato" : "base-sepolia";
81
+ summary.rail = isMpp ? "mpp" : "x402";
82
+
61
83
  line("Network", isMpp ? "Tempo Moderato (testnet)" : "Base Sepolia (testnet)");
62
84
  line("Rail", isMpp ? "mpp" : "x402");
63
85
 
@@ -110,6 +132,7 @@ export async function run(args = []) {
110
132
  } else {
111
133
  line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD`);
112
134
  }
135
+ summary.balance = { symbol: "pathUSD", usd_micros: balance };
113
136
  } else {
114
137
  // Base Sepolia: read USDC balance (existing behavior)
115
138
  const { createPublicClient, http } = await import("viem");
@@ -152,6 +175,7 @@ export async function run(args = []) {
152
175
  } else {
153
176
  line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
154
177
  }
178
+ summary.balance = { symbol: "USDC", usd_micros: balance };
155
179
  }
156
180
 
157
181
  // Show note if switching rails
@@ -176,19 +200,32 @@ export async function run(args = []) {
176
200
  if (tierInfo && tierInfo.tier && tierInfo.active) {
177
201
  const expiry = tierInfo.lease_expires_at ? tierInfo.lease_expires_at.split("T")[0] : "unknown";
178
202
  line("Tier", `${tierInfo.tier} (expires ${expiry})`);
203
+ summary.tier = { name: tierInfo.tier, expires: tierInfo.lease_expires_at || null };
179
204
  } else {
180
205
  line("Tier", "(none)");
206
+ summary.tier = null;
181
207
  }
182
208
 
183
- // 5. Projects
184
- line("Projects", `${Object.keys(store.projects).length} active`);
209
+ // 5. Projects — count locally saved project entries. Note: "saved" (not
210
+ // "active") — these are all projects in the keystore, regardless of whether
211
+ // the server considers them active.
212
+ summary.projects_saved = Object.keys(store.projects).length;
213
+ line("Projects", `${summary.projects_saved} saved`);
185
214
 
186
215
  // 6. Next step
187
- console.log();
216
+ write("");
217
+ const nextStep = (!tierInfo || !tierInfo.tier || !tierInfo.active)
218
+ ? "run402 tier set prototype"
219
+ : "run402 deploy --manifest app.json";
188
220
  if (!tierInfo || !tierInfo.tier || !tierInfo.active) {
189
- console.log(" Next: run402 tier set prototype");
221
+ write(" Next: run402 tier set prototype");
190
222
  } else {
191
- console.log(" Ready to deploy. Run: run402 deploy --manifest app.json");
223
+ write(" Ready to deploy. Run: run402 deploy --manifest app.json");
224
+ }
225
+ write("");
226
+ summary.next_step = nextStep;
227
+
228
+ if (jsonMode) {
229
+ console.log(JSON.stringify(summary, null, 2));
192
230
  }
193
- console.log();
194
231
  }
package/lib/projects.mjs CHANGED
@@ -141,13 +141,17 @@ async function sqlCmd(projectId, args = []) {
141
141
  const headers = { "Authorization": `Bearer ${p.service_key}`, "Content-Type": useParams ? "application/json" : "text/plain" };
142
142
  const body = useParams ? JSON.stringify({ sql, params }) : sql;
143
143
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers, body });
144
- console.log(JSON.stringify(await res.json(), null, 2));
144
+ const data = await res.json();
145
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
146
+ console.log(JSON.stringify(data, null, 2));
145
147
  }
146
148
 
147
149
  async function rest(projectId, table, queryParams) {
148
150
  const p = findProject(projectId);
149
151
  const res = await fetch(`${API}/rest/v1/${table}${queryParams ? '?' + queryParams : ''}`, { headers: { "apikey": p.anon_key } });
150
- console.log(JSON.stringify(await res.json(), null, 2));
152
+ const data = await res.json();
153
+ if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
154
+ console.log(JSON.stringify(data, null, 2));
151
155
  }
152
156
 
153
157
  async function usage(projectId) {
package/lib/status.mjs CHANGED
@@ -8,6 +8,7 @@ Usage:
8
8
 
9
9
  Displays:
10
10
  - Allowance address and funding status
11
+ - Wallet on-chain USDC/pathUSD balance (wallet_balance_usd_micros)
11
12
  - Billing balance (available + held)
12
13
  - Tier subscription (name, status, expiry)
13
14
  - Projects (from server, with fallback to local keystore)
@@ -16,6 +17,63 @@ Displays:
16
17
  Output is JSON. Requires an existing allowance (run 'run402 init' first).
17
18
  `;
18
19
 
20
+ // USDC / pathUSD constants (match allowance.mjs)
21
+ const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
22
+ const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
23
+ const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
24
+ const PATH_USD = "0x20c0000000000000000000000000000000000000";
25
+ const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
26
+
27
+ /**
28
+ * Read the on-chain wallet balance in USD micros for the current rail.
29
+ * For x402: read Base mainnet + Base Sepolia USDC and sum funded networks.
30
+ * For mpp: read pathUSD on Tempo Moderato.
31
+ * Returns null if every read fails (e.g. offline or RPC down).
32
+ */
33
+ async function readWalletBalanceUsdMicros(rail, address) {
34
+ try {
35
+ const { createPublicClient, http, defineChain } = await import("viem");
36
+ if (rail === "mpp") {
37
+ const tempoModerato = defineChain({
38
+ id: 42431,
39
+ name: "Tempo Moderato",
40
+ nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
41
+ rpcUrls: { default: { http: [TEMPO_RPC] } },
42
+ });
43
+ const client = createPublicClient({ chain: tempoModerato, transport: http() });
44
+ try {
45
+ const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [address] });
46
+ return Number(raw);
47
+ } catch { return null; }
48
+ }
49
+ // x402 rail — read Base mainnet + Base Sepolia in parallel; sum any that succeed.
50
+ const { base, baseSepolia } = await import("viem/chains");
51
+ const mainnetClient = createPublicClient({ chain: base, transport: http() });
52
+ const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
53
+ const [mainnet, sepolia] = await Promise.all([
54
+ mainnetClient.readContract({ address: USDC_MAINNET, abi: USDC_ABI, functionName: "balanceOf", args: [address] }).then(Number).catch(() => null),
55
+ sepoliaClient.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [address] }).then(Number).catch(() => null),
56
+ ]);
57
+ if (mainnet === null && sepolia === null) return null;
58
+ return (mainnet || 0) + (sepolia || 0);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Normalize a project entry to the agreed-on shape: always expose `project_id`
66
+ * (matching `projects list`). The remote /wallets/v1/:wallet/projects endpoint
67
+ * returns entries keyed as `id`, so we map them here and drop the raw `id`
68
+ * field to avoid having two aliases for the same identity.
69
+ */
70
+ function normalizeProject(raw) {
71
+ if (!raw || typeof raw !== "object") return raw;
72
+ const projectId = raw.project_id || raw.id;
73
+ const { id: _dropId, project_id: _dropPid, ...rest } = raw;
74
+ return { project_id: projectId, ...rest };
75
+ }
76
+
19
77
  export async function run(args = []) {
20
78
  if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
21
79
  const allowance = readAllowance();
@@ -26,14 +84,16 @@ export async function run(args = []) {
26
84
 
27
85
  const wallet = allowance.address.toLowerCase();
28
86
  const authHeaders = getAllowanceAuthHeaders("/tiers/v1/status");
87
+ const rail = allowance.rail || "x402";
29
88
 
30
- // Parallel API calls: tier + billing balance + server-side projects
31
- const [tierRes, balanceRes, projectsRes] = await Promise.all([
89
+ // Parallel API calls: tier + billing balance + server-side projects + on-chain wallet balance
90
+ const [tierRes, balanceRes, projectsRes, walletBalance] = await Promise.all([
32
91
  authHeaders
33
92
  ? fetch(`${API}/tiers/v1/status`, { headers: { ...authHeaders } }).catch(() => null)
34
93
  : null,
35
94
  fetch(`${API}/billing/v1/accounts/${wallet}`).catch(() => null),
36
95
  fetch(`${API}/wallets/v1/${wallet}/projects`).catch(() => null),
96
+ readWalletBalanceUsdMicros(rail, allowance.address),
37
97
  ]);
38
98
 
39
99
  const tier = tierRes?.ok ? await tierRes.json() : null;
@@ -44,18 +104,29 @@ export async function run(args = []) {
44
104
  const store = loadKeyStore();
45
105
  const activeId = getActiveProjectId();
46
106
 
107
+ const projects = remote?.projects
108
+ ? remote.projects.map(normalizeProject)
109
+ : Object.keys(store.projects).map(id => ({ project_id: id }));
110
+
47
111
  const result = {
48
112
  allowance: {
49
113
  address: allowance.address,
50
114
  funded: allowance.funded || false,
51
115
  },
116
+ rail,
52
117
  tier: tier && tier.tier
53
118
  ? { name: tier.tier, status: tier.status, expires: tier.lease_expires_at }
54
119
  : null,
55
- balance: billing && billing.exists
120
+ // GH-32: `balance` used to mean the billing-account balance, which
121
+ // confused people who expected their on-chain wallet balance. Split into
122
+ // two unambiguous fields:
123
+ // - billing: credits held by Run402 (available + held), null if no account
124
+ // - wallet_balance_usd_micros: on-chain USDC/pathUSD, null if RPC fails
125
+ billing: billing && billing.exists
56
126
  ? { available_usd_micros: billing.available_usd_micros, held_usd_micros: billing.held_usd_micros }
57
127
  : null,
58
- projects: remote?.projects || Object.keys(store.projects).map(id => ({ id })),
128
+ wallet_balance_usd_micros: walletBalance,
129
+ projects,
59
130
  active_project: activeId || null,
60
131
  };
61
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.35.0",
3
+ "version": "1.35.1",
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": {