run402 1.13.5 → 1.14.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
@@ -19,7 +19,8 @@ Usage:
19
19
  run402 <command> [subcommand] [options]
20
20
 
21
21
  Commands:
22
- init Set up allowance, funding, and check tier status
22
+ init Set up allowance, funding, and check tier status (x402 default)
23
+ init mpp Set up with MPP payment rail (Tempo Moderato testnet)
23
24
  status Show full account state (allowance, balance, tier, projects)
24
25
  allowance Manage your agent allowance (create, fund, balance, status)
25
26
  tier Manage tier subscription (status, set)
@@ -31,7 +32,7 @@ Commands:
31
32
  sites Deploy static sites
32
33
  subdomains Manage custom subdomains (claim, list, delete)
33
34
  apps Browse and manage the app marketplace
34
- image Generate AI images via x402 micropayments
35
+ image Generate AI images via x402 or MPP micropayments
35
36
  message Send messages to Run402 developers
36
37
  agent Manage agent identity (contact info)
37
38
 
@@ -48,7 +49,8 @@ Examples:
48
49
  run402 image generate "a startup mascot, pixel art" --output logo.png
49
50
 
50
51
  Getting started:
51
- run402 init Set up everything in one command
52
+ run402 init Set up with x402 (Base Sepolia)
53
+ run402 init mpp Set up with MPP (Tempo Moderato)
52
54
  run402 tier set prototype Subscribe to a tier
53
55
  run402 deploy --manifest app.json
54
56
  `;
package/lib/allowance.mjs CHANGED
@@ -6,24 +6,25 @@ Usage:
6
6
  run402 allowance <subcommand>
7
7
 
8
8
  Subcommands:
9
- status Show allowance address, network, and funding status
9
+ status Show allowance address, network, rail, and funding status
10
10
  create Generate a new allowance and save it locally
11
- fund Request test USDC from the Run402 faucet (Base Sepolia)
12
- balance Show on-chain USDC (mainnet + testnet) and Run402 billing balance
11
+ fund Request test funds from the faucet (Base Sepolia or Tempo)
12
+ balance Show on-chain balances and Run402 billing balance
13
13
  export Print the allowance address (useful for scripting)
14
14
  checkout Create a billing checkout session (--amount <usd_micros>)
15
15
  history View billing transaction history (--limit <n>)
16
16
 
17
17
  Notes:
18
- - Agent allowance is stored locally at ~/.run402/allowance.json
19
- - The allowance works on any EVM chain (currently Run402 uses Base Mainnet and Sepolia for testnet)
20
- - You need to create and fund an allowance before any x402 transaction with Run402
18
+ - Agent allowance is stored locally at ~/.config/run402/allowance.json
19
+ - The allowance works on any EVM chain (Base for x402, Tempo for MPP)
20
+ - Use 'run402 init' for x402 or 'run402 init mpp' for MPP rail
21
21
 
22
22
  Examples:
23
23
  run402 allowance create
24
24
  run402 allowance status
25
25
  run402 allowance fund
26
26
  run402 allowance export
27
+ run402 allowance balance
27
28
  run402 allowance checkout --amount 5000000
28
29
  run402 allowance history --limit 10
29
30
  `;
@@ -31,12 +32,20 @@ Examples:
31
32
  const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
32
33
  const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
33
34
  const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
35
+ const PATH_USD = "0x20c0000000000000000000000000000000000000";
36
+ const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
34
37
 
35
38
  async function loadDeps() {
36
39
  const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
37
- const { createPublicClient, http } = await import("viem");
40
+ const { createPublicClient, http, defineChain } = await import("viem");
38
41
  const { base, baseSepolia } = await import("viem/chains");
39
- return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, base, baseSepolia };
42
+ const tempoModerato = defineChain({
43
+ id: 42431,
44
+ name: "Tempo Moderato",
45
+ nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
46
+ rpcUrls: { default: { http: [TEMPO_RPC] } },
47
+ });
48
+ return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, base, baseSepolia, tempoModerato };
40
49
  }
41
50
 
42
51
  async function status() {
@@ -45,7 +54,7 @@ async function status() {
45
54
  console.log(JSON.stringify({ status: "no_wallet", message: "No agent allowance found. Run: run402 allowance create" }));
46
55
  return;
47
56
  }
48
- console.log(JSON.stringify({ status: "ok", address: w.address, created: w.created, funded: w.funded || false, path: ALLOWANCE_FILE }));
57
+ console.log(JSON.stringify({ status: "ok", address: w.address, created: w.created, funded: w.funded || false, rail: w.rail || "x402", path: ALLOWANCE_FILE }));
49
58
  }
50
59
 
51
60
  async function create() {
@@ -64,6 +73,37 @@ async function fund() {
64
73
  const w = readAllowance();
65
74
  if (!w) { console.log(JSON.stringify({ status: "error", message: "No agent allowance. Run: run402 allowance create" })); process.exit(1); }
66
75
 
76
+ if (w.rail === "mpp") {
77
+ // Tempo Moderato faucet — instant, no polling needed
78
+ const { createPublicClient, http, tempoModerato } = await loadDeps();
79
+ const client = createPublicClient({ chain: tempoModerato, transport: http() });
80
+ const before = await readUsdcBalance(client, PATH_USD, w.address).catch(() => 0);
81
+
82
+ const res = await fetch(TEMPO_RPC, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json" },
85
+ body: JSON.stringify({ jsonrpc: "2.0", method: "tempo_fundAddress", params: [w.address], id: 1 }),
86
+ });
87
+ const data = await res.json();
88
+ if (data.error) {
89
+ console.log(JSON.stringify({ status: "error", message: data.error.message || "Tempo faucet failed" }));
90
+ process.exit(1);
91
+ }
92
+
93
+ // Re-read balance once (instant confirmation)
94
+ const now = await readUsdcBalance(client, PATH_USD, w.address).catch(() => before);
95
+ saveAllowance({ ...w, funded: true, lastFaucet: new Date().toISOString() });
96
+ console.log(JSON.stringify({
97
+ address: w.address,
98
+ rail: "mpp",
99
+ onchain: {
100
+ "tempo-moderato_pathusd_micros": now,
101
+ },
102
+ }, null, 2));
103
+ return;
104
+ }
105
+
106
+ // Default: Base Sepolia faucet (existing behavior)
67
107
  const { createPublicClient, http, baseSepolia } = await loadDeps();
68
108
  const client = createPublicClient({ chain: baseSepolia, transport: http() });
69
109
  const before = await readUsdcBalance(client, USDC_SEPOLIA, w.address).catch(() => 0);
@@ -83,6 +123,7 @@ async function fund() {
83
123
  saveAllowance({ ...w, funded: true, lastFaucet: new Date().toISOString() });
84
124
  console.log(JSON.stringify({
85
125
  address: w.address,
126
+ rail: w.rail || "x402",
86
127
  onchain: {
87
128
  "base-sepolia_usd_micros": now,
88
129
  },
@@ -104,13 +145,15 @@ async function balance() {
104
145
  const w = readAllowance();
105
146
  if (!w) { console.log(JSON.stringify({ status: "error", message: "No agent allowance. Run: run402 allowance create" })); process.exit(1); }
106
147
 
107
- const { createPublicClient, http, base, baseSepolia } = await loadDeps();
148
+ const { createPublicClient, http, base, baseSepolia, tempoModerato } = await loadDeps();
108
149
  const mainnetClient = createPublicClient({ chain: base, transport: http() });
109
150
  const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
151
+ const tempoClient = createPublicClient({ chain: tempoModerato, transport: http() });
110
152
 
111
- const [mainnetUsdc, sepoliaUsdc, billingRes] = await Promise.all([
153
+ const [mainnetUsdc, sepoliaUsdc, tempoPathUsd, billingRes] = await Promise.all([
112
154
  readUsdcBalance(mainnetClient, USDC_MAINNET, w.address).catch(() => null),
113
155
  readUsdcBalance(sepoliaClient, USDC_SEPOLIA, w.address).catch(() => null),
156
+ readUsdcBalance(tempoClient, PATH_USD, w.address).catch(() => null),
114
157
  fetch(`${API}/billing/v1/accounts/${w.address.toLowerCase()}`),
115
158
  ]);
116
159
 
@@ -118,9 +161,11 @@ async function balance() {
118
161
 
119
162
  console.log(JSON.stringify({
120
163
  address: w.address,
164
+ rail: w.rail || "x402",
121
165
  onchain: {
122
166
  "base-mainnet_usd_micros": mainnetUsdc,
123
167
  "base-sepolia_usd_micros": sepoliaUsdc,
168
+ "tempo-moderato_pathusd_micros": tempoPathUsd,
124
169
  },
125
170
  run402: billing ? { balance_usd_micros: billing.available_usd_micros } : "no billing account",
126
171
  }, null, 2));
package/lib/init.mjs CHANGED
@@ -4,16 +4,19 @@ import { mkdirSync } from "fs";
4
4
 
5
5
  const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
6
6
  const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
7
+ const PATH_USD = "0x20c0000000000000000000000000000000000000";
8
+ const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
7
9
 
8
10
  const HELP = `run402 init — Set up allowance, funding, and check tier status
9
11
 
10
12
  Usage:
11
- run402 init
13
+ run402 init Set up with x402 (Base Sepolia) — default
14
+ run402 init mpp Set up with MPP (Tempo Moderato)
12
15
 
13
16
  Steps (idempotent — safe to re-run):
14
17
  1. Creates config directory (~/.config/run402)
15
18
  2. Creates agent allowance if none exists
16
- 3. Checks on-chain USDC balance; requests faucet if zero
19
+ 3. Checks on-chain balance; requests faucet if zero
17
20
  4. Shows current tier subscription status
18
21
  5. Lists local project count
19
22
  6. Suggests next step (tier set or deploy)
@@ -26,6 +29,7 @@ function line(label, value) { console.log(` ${label.padEnd(10)} ${value}`); }
26
29
 
27
30
  export async function run(args = []) {
28
31
  if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
32
+ const isMpp = args[0] === "mpp";
29
33
  console.log();
30
34
 
31
35
  // 1. Config directory
@@ -34,58 +38,126 @@ export async function run(args = []) {
34
38
 
35
39
  // 2. Allowance
36
40
  let allowance = readAllowance();
41
+ const previousRail = allowance?.rail;
37
42
  if (!allowance) {
38
43
  const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
39
44
  const privateKey = generatePrivateKey();
40
45
  const account = privateKeyToAccount(privateKey);
41
- allowance = { address: account.address, privateKey, created: new Date().toISOString(), funded: false };
46
+ allowance = { address: account.address, privateKey, created: new Date().toISOString(), funded: false, rail: isMpp ? "mpp" : "x402" };
42
47
  saveAllowance(allowance);
43
48
  line("Allowance", `${short(allowance.address)} (created)`);
44
49
  } else {
50
+ // Update rail if switching
51
+ if ((isMpp && allowance.rail !== "mpp") || (!isMpp && allowance.rail === "mpp")) {
52
+ allowance = { ...allowance, rail: isMpp ? "mpp" : "x402" };
53
+ saveAllowance(allowance);
54
+ } else if (!allowance.rail) {
55
+ allowance = { ...allowance, rail: isMpp ? "mpp" : "x402" };
56
+ saveAllowance(allowance);
57
+ }
45
58
  line("Allowance", short(allowance.address));
46
59
  }
47
60
 
48
- // 3. Balance check on-chain, faucet if zero
49
- const { createPublicClient, http } = await import("viem");
50
- const { baseSepolia } = await import("viem/chains");
51
- const client = createPublicClient({ chain: baseSepolia, transport: http() });
61
+ line("Network", isMpp ? "Tempo Moderato (testnet)" : "Base Sepolia (testnet)");
62
+ line("Rail", isMpp ? "mpp" : "x402");
52
63
 
64
+ // 3. Balance — check on-chain, faucet if zero
53
65
  let balance = 0;
54
- try {
55
- const raw = await client.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
56
- balance = Number(raw);
57
- } catch {}
58
66
 
59
- if (balance === 0) {
60
- line("Balance", "0 USDC requesting faucet...");
61
- const res = await fetch(`${API}/faucet/v1`, {
62
- method: "POST",
63
- headers: { "Content-Type": "application/json" },
64
- body: JSON.stringify({ address: allowance.address }),
67
+ if (isMpp) {
68
+ // Tempo Moderato: read pathUSD balance
69
+ const { createPublicClient, http, defineChain } = await import("viem");
70
+ const tempoModerato = defineChain({
71
+ id: 42431,
72
+ name: "Tempo Moderato",
73
+ nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
74
+ rpcUrls: { default: { http: [TEMPO_RPC] } },
65
75
  });
66
- if (res.ok) {
67
- // Poll for up to 30s
68
- for (let i = 0; i < 30; i++) {
69
- await new Promise(r => setTimeout(r, 1000));
70
- try {
71
- const raw = await client.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
72
- balance = Number(raw);
73
- if (balance > 0) break;
74
- } catch {}
76
+ const client = createPublicClient({ chain: tempoModerato, transport: http() });
77
+
78
+ try {
79
+ const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
80
+ balance = Number(raw);
81
+ } catch {}
82
+
83
+ if (balance === 0) {
84
+ line("Balance", "0 pathUSD — requesting Tempo faucet...");
85
+ try {
86
+ const res = await fetch(TEMPO_RPC, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({ jsonrpc: "2.0", method: "tempo_fundAddress", params: [allowance.address], id: 1 }),
90
+ });
91
+ const data = await res.json();
92
+ if (data.result) {
93
+ // Tempo faucet is instant — re-read balance once
94
+ try {
95
+ const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
96
+ balance = Number(raw);
97
+ } catch {}
98
+ saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
99
+ if (balance > 0) {
100
+ line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD (funded)`);
101
+ } else {
102
+ line("Balance", "faucet sent — checking balance...");
103
+ }
104
+ } else {
105
+ line("Balance", `faucet failed: ${data.error?.message || "unknown error"}`);
106
+ }
107
+ } catch (err) {
108
+ line("Balance", `faucet error: ${err.message}`);
75
109
  }
76
- saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
77
- if (balance > 0) {
78
- line("Balance", `${(balance / 1e6).toFixed(2)} USDC (funded)`);
110
+ } else {
111
+ line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD`);
112
+ }
113
+ } else {
114
+ // Base Sepolia: read USDC balance (existing behavior)
115
+ const { createPublicClient, http } = await import("viem");
116
+ const { baseSepolia } = await import("viem/chains");
117
+ const client = createPublicClient({ chain: baseSepolia, transport: http() });
118
+
119
+ try {
120
+ const raw = await client.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
121
+ balance = Number(raw);
122
+ } catch {}
123
+
124
+ if (balance === 0) {
125
+ line("Balance", "0 USDC — requesting faucet...");
126
+ const res = await fetch(`${API}/faucet/v1`, {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ address: allowance.address }),
130
+ });
131
+ if (res.ok) {
132
+ // Poll for up to 30s
133
+ for (let i = 0; i < 30; i++) {
134
+ await new Promise(r => setTimeout(r, 1000));
135
+ try {
136
+ const raw = await client.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
137
+ balance = Number(raw);
138
+ if (balance > 0) break;
139
+ } catch {}
140
+ }
141
+ saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
142
+ if (balance > 0) {
143
+ line("Balance", `${(balance / 1e6).toFixed(2)} USDC (funded)`);
144
+ } else {
145
+ line("Balance", "faucet sent — not yet confirmed on-chain");
146
+ }
79
147
  } else {
80
- line("Balance", "faucet sent not yet confirmed on-chain");
148
+ const data = await res.json().catch(() => ({}));
149
+ const msg = data.error || data.message || `HTTP ${res.status}`;
150
+ line("Balance", `faucet failed: ${msg}`);
81
151
  }
82
152
  } else {
83
- const data = await res.json().catch(() => ({}));
84
- const msg = data.error || data.message || `HTTP ${res.status}`;
85
- line("Balance", `faucet failed: ${msg}`);
153
+ line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
86
154
  }
87
- } else {
88
- line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
155
+ }
156
+
157
+ // Show note if switching rails
158
+ if (previousRail && previousRail !== (isMpp ? "mpp" : "x402")) {
159
+ const prev = previousRail === "mpp" ? "Tempo pathUSD" : "Base Sepolia USDC";
160
+ line("Note", `Switched from ${previousRail} — ${prev} balance still available if you switch back`);
89
161
  }
90
162
 
91
163
  // 4. Tier status
@@ -101,7 +173,7 @@ export async function run(args = []) {
101
173
  }
102
174
  } catch {}
103
175
 
104
- if (tierInfo && tierInfo.tier && tierInfo.status === "active") {
176
+ if (tierInfo && tierInfo.tier && tierInfo.active) {
105
177
  const expiry = tierInfo.lease_expires_at ? tierInfo.lease_expires_at.split("T")[0] : "unknown";
106
178
  line("Tier", `${tierInfo.tier} (expires ${expiry})`);
107
179
  } else {
@@ -113,7 +185,7 @@ export async function run(args = []) {
113
185
 
114
186
  // 6. Next step
115
187
  console.log();
116
- if (!tierInfo || !tierInfo.tier || tierInfo.status !== "active") {
188
+ if (!tierInfo || !tierInfo.tier || !tierInfo.active) {
117
189
  console.log(" Next: run402 tier set prototype");
118
190
  } else {
119
191
  console.log(" Ready to deploy. Run: run402 deploy --manifest app.json");
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Shared x402 payment wrapper for CLI commands that need paid fetch.
3
- * Uses viem for allowance signing + @x402/fetch for payment wrapping.
4
- * Registers both Base mainnet (eip155:8453) and Base Sepolia (eip155:84532)
5
- * so the x402 client matches whichever network the server offers.
2
+ * Shared payment wrapper for CLI commands that need paid fetch.
3
+ * Branches on allowance rail:
4
+ * - "mpp": uses mppx.fetch (Tempo pathUSD)
5
+ * - "x402" (default): uses @x402/fetch (Base USDC)
6
6
  */
7
7
 
8
8
  import { readAllowance, ALLOWANCE_FILE } from "./config.mjs";
@@ -15,12 +15,23 @@ export async function setupPaidFetch() {
15
15
  }
16
16
  const allowance = readAllowance();
17
17
  const { privateKeyToAccount } = await import("viem/accounts");
18
+ const account = privateKeyToAccount(allowance.privateKey);
19
+
20
+ if (allowance.rail === "mpp") {
21
+ const { Mppx, tempo } = await import("mppx/client");
22
+ const mppx = Mppx.create({
23
+ polyfill: false,
24
+ methods: [tempo({ account })],
25
+ });
26
+ return mppx.fetch;
27
+ }
28
+
29
+ // Default: x402 (existing behavior)
18
30
  const { createPublicClient, http } = await import("viem");
19
31
  const { base, baseSepolia } = await import("viem/chains");
20
32
  const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
21
33
  const { ExactEvmScheme } = await import("@x402/evm/exact/client");
22
34
  const { toClientEvmSigner } = await import("@x402/evm");
23
- const account = privateKeyToAccount(allowance.privateKey);
24
35
 
25
36
  const mainnetClient = createPublicClient({ chain: base, transport: http() });
26
37
  const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
package/lib/tier.mjs CHANGED
@@ -38,7 +38,14 @@ async function set(tierName) {
38
38
  if (!tierName) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 tier set <prototype|hobby|team>" })); process.exit(1); }
39
39
  const fetchPaid = await setupPaidFetch();
40
40
  const res = await fetchPaid(`${API}/tiers/v1/${tierName}`, { method: "POST", headers: { "Content-Type": "application/json" } });
41
- const data = await res.json();
41
+ const text = await res.text();
42
+ let data;
43
+ try {
44
+ data = JSON.parse(text);
45
+ } catch {
46
+ console.error(JSON.stringify({ status: "error", http: res.status, message: "Non-JSON response from server", body: text.slice(0, 500) }));
47
+ process.exit(1);
48
+ }
42
49
  if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
43
50
  console.log(JSON.stringify(data, null, 2));
44
51
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.13.5",
4
- "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 micropayments.",
3
+ "version": "1.14.0",
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": {
7
7
  "run402": "cli.mjs"
@@ -19,6 +19,7 @@
19
19
  "@noble/hashes": "^2.0.1",
20
20
  "@x402/evm": "^2.6.0",
21
21
  "@x402/fetch": "^2.6.0",
22
+ "mppx": "^0.4.7",
22
23
  "viem": "^2.47.1"
23
24
  },
24
25
  "engines": {