Skip to content

Commit e86dcfd

Browse files
MajorTalclaude
andcommitted
Replace renew_project with set_tier and add tier CLI module
The gateway consolidated subscribe/renew/upgrade into a single POST /tiers/v1/:tier endpoint that auto-detects the action. - Delete renew_project MCP tool, add set_tier (wallet-level, not project-level) - Add cli/lib/tier.mjs and openclaw/scripts/tier.mjs with status + set subcommands - Remove renew from projects modules and tier/set-tier from wallet modules - Update SURFACE, errors, README, SKILL.md, and deploy-function test mock URL Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2e12509 commit e86dcfd

16 files changed

Lines changed: 251 additions & 256 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ npx run402-mcp
6060
| `publish_app` | Publish a project as a forkable app. |
6161
| `list_versions` | List published versions of a project. |
6262
| `get_quote` | Get tier pricing. Free, no auth required. |
63-
| `renew_project` | Renew lease. Handles x402 payment. |
63+
| `set_tier` | Subscribe, renew, or upgrade wallet tier. Auto-detects action. Handles x402 payment. |
6464
| `archive_project` | Archive a project and remove from local key store. |
6565
| `check_balance` | Check billing account balance for a wallet address. |
6666
| `list_projects` | List all active projects for a wallet address. |
@@ -155,7 +155,7 @@ claude mcp add run402 -- npx -y run402-mcp
155155
1. **Provision** — Call `provision_postgres_project` to create a database. The server handles x402 payment negotiation and stores credentials locally.
156156
2. **Build** — Use `run_sql` to create tables, `rest_query` to insert/query data, and `upload_file` for storage.
157157
3. **Deploy** — Use `deploy_site` for static sites, `deploy_function` for serverless functions, or `bundle_deploy` for a full-stack app in one call.
158-
4. **Renew** — Call `renew_project` before your lease expires.
158+
4. **Renew** — Call `set_tier` before your lease expires.
159159

160160
### Payment Flow
161161

cli/cli.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ Usage:
1414
1515
Commands:
1616
wallet Manage your x402 wallet (create, fund, balance, status)
17-
projects Manage projects (provision, list, query, inspect, renew, delete)
17+
tier Manage tier subscription (status, set)
18+
projects Manage projects (provision, list, query, inspect, delete)
1819
deploy Deploy a full-stack app or static site (Postgres + hosting)
1920
functions Manage serverless functions (deploy, invoke, logs, list, delete)
2021
secrets Manage project secrets (set, list, delete)
@@ -55,6 +56,11 @@ switch (cmd) {
5556
await run(sub, rest);
5657
break;
5758
}
59+
case "tier": {
60+
const { run } = await import("./lib/tier.mjs");
61+
await run(sub, rest);
62+
break;
63+
}
5864
case "projects": {
5965
const { run } = await import("./lib/projects.mjs");
6066
await run(sub, rest);

cli/lib/projects.mjs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ Subcommands:
1515
usage <id> Show compute/storage usage for a project
1616
schema <id> Inspect the database schema
1717
rls <id> <template> <tables_json> Apply Row-Level Security policies
18-
renew <id> Extend the project lease (pays via x402)
1918
delete <id> Delete a project and remove it from local state
2019
2120
Examples:
@@ -28,13 +27,12 @@ Examples:
2827
run402 projects usage abc123
2928
run402 projects schema abc123
3029
run402 projects rls abc123 public_read '[{"table":"posts"}]'
31-
run402 projects renew abc123
3230
run402 projects delete abc123
3331
3432
Notes:
3533
- <id> is the project_id shown in 'run402 projects list'
3634
- 'rest' uses PostgREST query syntax (table name + optional query string)
37-
- 'renew' and 'provision' require a funded wallet — payment is automatic via x402
35+
- 'provision' requires a funded wallet — payment is automatic via x402
3836
- RLS templates: user_owns_rows, public_read, public_read_write
3937
`;
4038

@@ -142,19 +140,6 @@ async function schema(projectId) {
142140
console.log(JSON.stringify(data, null, 2));
143141
}
144142

145-
async function renew(projectId) {
146-
const p = findProject(projectId);
147-
const tier = p.tier || "prototype";
148-
const fetchPaid = await setupPaidFetch();
149-
const res = await fetchPaid(`${API}/tiers/v1/renew/${tier}`, { method: "POST", headers: { "Content-Type": "application/json" } });
150-
const data = await res.json();
151-
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
152-
const projects = loadProjects();
153-
const idx = projects.findIndex(pr => pr.project_id === projectId);
154-
if (idx >= 0 && data.lease_expires_at) { projects[idx].lease_expires_at = data.lease_expires_at; saveProjects(projects); }
155-
console.log(JSON.stringify(data, null, 2));
156-
}
157-
158143
async function deleteProject(projectId) {
159144
const p = findProject(projectId);
160145
const res = await fetch(`${API}/projects/v1/${projectId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${p.service_key}` } });
@@ -181,7 +166,6 @@ export async function run(sub, args) {
181166
case "usage": await usage(args[0]); break;
182167
case "schema": await schema(args[0]); break;
183168
case "rls": await rls(args[0], args[1], args[2]); break;
184-
case "renew": await renew(args[0]); break;
185169
case "delete": await deleteProject(args[0]); break;
186170
default:
187171
console.error(`Unknown subcommand: ${sub}\n`);

cli/lib/tier.mjs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { readWallet, WALLET_FILE, API } from "./config.mjs";
2+
import { existsSync } from "fs";
3+
4+
const HELP = `run402 tier — Manage your Run402 tier subscription
5+
6+
Usage:
7+
run402 tier <subcommand> [args...]
8+
9+
Subcommands:
10+
status Show current tier (tier name, status, expiry)
11+
set <tier> Subscribe, renew, or upgrade (pays via x402)
12+
13+
Tiers: prototype ($0.10/7d), hobby ($5/30d), team ($20/30d)
14+
15+
The server auto-detects the action based on your wallet state:
16+
- No tier or expired → subscribe
17+
- Same tier, active → renew (extends from expiry)
18+
- Higher tier → upgrade (prorated refund to allowance)
19+
- Lower tier, active → rejected (wait for expiry)
20+
21+
Examples:
22+
run402 tier status
23+
run402 tier set prototype
24+
run402 tier set hobby
25+
`;
26+
27+
async function setupPaidFetch() {
28+
if (!existsSync(WALLET_FILE)) {
29+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
30+
process.exit(1);
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+
return wrapFetchWithPayment(fetch, client);
45+
}
46+
47+
async function status() {
48+
const w = readWallet();
49+
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
50+
const { privateKeyToAccount } = await import("viem/accounts");
51+
const account = privateKeyToAccount(w.privateKey);
52+
const timestamp = Math.floor(Date.now() / 1000).toString();
53+
const signature = await account.signMessage({ message: `run402:${timestamp}` });
54+
const res = await fetch(`${API}/tiers/v1/status`, {
55+
headers: { "X-Run402-Wallet": account.address, "X-Run402-Signature": signature, "X-Run402-Timestamp": timestamp },
56+
});
57+
const data = await res.json();
58+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
59+
console.log(JSON.stringify(data, null, 2));
60+
}
61+
62+
async function set(tierName) {
63+
if (!tierName) { console.error(JSON.stringify({ status: "error", message: "Usage: run402 tier set <prototype|hobby|team>" })); process.exit(1); }
64+
const fetchPaid = await setupPaidFetch();
65+
const res = await fetchPaid(`${API}/tiers/v1/${tierName}`, { method: "POST", headers: { "Content-Type": "application/json" } });
66+
const data = await res.json();
67+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
68+
console.log(JSON.stringify(data, null, 2));
69+
}
70+
71+
export async function run(sub, args) {
72+
if (!sub || sub === '--help' || sub === '-h') {
73+
console.log(HELP);
74+
process.exit(0);
75+
}
76+
switch (sub) {
77+
case "status": await status(); break;
78+
case "set": await set(args[0]); break;
79+
default:
80+
console.error(`Unknown subcommand: ${sub}\n`);
81+
console.log(HELP);
82+
process.exit(1);
83+
}
84+
}

cli/lib/wallet.mjs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ Subcommands:
1010
create Generate a new wallet and save it locally
1111
fund Request test USDC from the Run402 faucet (Base Sepolia)
1212
balance Show on-chain USDC (mainnet + testnet) and Run402 billing balance
13-
tier Show current tier subscription (tier, status, expiry)
1413
export Print the wallet address (useful for scripting)
1514
checkout Create a billing checkout session (--amount <usd_micros>)
1615
history View billing transaction history (--limit <n>)
@@ -127,21 +126,6 @@ async function balance() {
127126
}, null, 2));
128127
}
129128

130-
async function tier() {
131-
const w = readWallet();
132-
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
133-
const { privateKeyToAccount } = await loadDeps();
134-
const account = privateKeyToAccount(w.privateKey);
135-
const timestamp = Math.floor(Date.now() / 1000).toString();
136-
const signature = await account.signMessage({ message: `run402:${timestamp}` });
137-
const res = await fetch(`${API}/tiers/v1/status`, {
138-
headers: { "X-Run402-Wallet": account.address, "X-Run402-Signature": signature, "X-Run402-Timestamp": timestamp },
139-
});
140-
const data = await res.json();
141-
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
142-
console.log(JSON.stringify(data, null, 2));
143-
}
144-
145129
async function exportAddr() {
146130
const w = readWallet();
147131
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet." })); process.exit(1); }
@@ -189,7 +173,6 @@ export async function run(sub, args) {
189173
case "create": await create(); break;
190174
case "fund": await fund(); break;
191175
case "balance": await balance(); break;
192-
case "tier": await tier(); break;
193176
case "export": await exportAddr(); break;
194177
case "checkout": await checkout(args); break;
195178
case "history": await history(args); break;

openclaw/SKILL.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,6 @@ node <skill_dir>/scripts/projects.mjs usage <project_id>
503503
# Inspect schema (tables, columns, RLS)
504504
node <skill_dir>/scripts/projects.mjs schema <project_id>
505505

506-
# Renew lease (x402 payment)
507-
node <skill_dir>/scripts/projects.mjs renew <project_id>
508-
509506
# Delete (archive and delete)
510507
node <skill_dir>/scripts/projects.mjs delete <project_id>
511508

@@ -521,7 +518,7 @@ node <skill_dir>/scripts/projects.mjs rest <project_id> todos "done=eq.false&ord
521518
- **Expired (day 0)**: read-only for 7 days
522519
- **Grace ends (day 7)**: archived (no access)
523520
- **Day 37**: permanent deletion
524-
- **Renew anytime** before deletion via `POST /projects/v1/:id/renew`
521+
- **Renew anytime** before deletion via `POST /tiers/v1/:tier`
525522

526523
---
527524

@@ -559,7 +556,7 @@ curl -X POST https://api.run402.com/projects/v1 \
559556
-H "X-402-Payment: <payment>"
560557
```
561558

562-
Supported on: `/projects/v1`, `/projects/v1/create/:tier`, `/projects/v1/:id/renew`, `/deployments/v1`, `/message/v1`, `/generate-image/v1`, `/deploy/v1/:tier`.
559+
Supported on: `/projects/v1`, `/projects/v1/create/:tier`, `/tiers/v1/:tier`, `/deployments/v1`, `/message/v1`, `/generate-image/v1`, `/deploy/v1/:tier`.
563560

564561
**Always include an Idempotency-Key when provisioning or renewing.**
565562

@@ -596,7 +593,7 @@ Check pricing: `POST /projects/v1/quote` (free, no auth).
596593

597594
| Auth Method | Header | Used For |
598595
|-------------|--------|----------|
599-
| x402 payment | (automatic via x402 client) | `POST /projects/v1`, `/deployments/v1`, `/generate-image/v1`, `/message/v1`, `/projects/v1/:id/renew`, `/deploy/v1/:tier` |
596+
| x402 payment | (automatic via x402 client) | `POST /projects/v1`, `/deployments/v1`, `/generate-image/v1`, `/message/v1`, `/tiers/v1/:tier`, `/deploy/v1/:tier` |
600597
| service_key | `Authorization: Bearer {service_key}` | `/projects/v1/admin/:id/*`, `POST /subdomains/v1`, `DELETE /subdomains/v1/:name` |
601598
| apikey | `apikey: {anon_key or service_key or access_token}` | `/rest/v1/*`, `/auth/v1/*`, `/storage/v1/*` |
602599

openclaw/scripts/projects.mjs

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -137,26 +137,6 @@ async function schema(projectId) {
137137
console.log(JSON.stringify(data, null, 2));
138138
}
139139

140-
async function renew(projectId) {
141-
const p = findProject(projectId);
142-
const tier = p.tier || "prototype";
143-
const fetchPaid = await setupPaidFetch();
144-
const res = await fetchPaid(`${API}/tiers/v1/renew/${tier}`, {
145-
method: "POST",
146-
headers: { "Content-Type": "application/json" },
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-
151-
const projects = loadProjects();
152-
const idx = projects.findIndex(pr => pr.project_id === projectId);
153-
if (idx >= 0 && data.lease_expires_at) {
154-
projects[idx].lease_expires_at = data.lease_expires_at;
155-
saveProjects(projects);
156-
}
157-
console.log(JSON.stringify(data, null, 2));
158-
}
159-
160140
async function deleteProject(projectId) {
161141
const p = findProject(projectId);
162142
const res = await fetch(`${API}/projects/v1/${projectId}`, {
@@ -183,9 +163,8 @@ switch (cmd) {
183163
case "usage": await usage(args[0]); break;
184164
case "schema": await schema(args[0]); break;
185165
case "rls": await rls(args[0], args[1], args[2]); break;
186-
case "renew": await renew(args[0]); break;
187166
case "delete": await deleteProject(args[0]); break;
188167
default:
189-
console.log("Usage: node projects.mjs <quote|provision|list|sql|rest|usage|schema|rls|renew|delete> [args...]");
168+
console.log("Usage: node projects.mjs <quote|provision|list|sql|rest|usage|schema|rls|delete> [args...]");
190169
process.exit(1);
191170
}

openclaw/scripts/tier.mjs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Run402 tier manager — check and set tier subscription.
4+
*
5+
* Usage:
6+
* node tier.mjs status # Show current tier
7+
* node tier.mjs set <tier> # Subscribe, renew, or upgrade (x402 payment)
8+
*/
9+
10+
import { readWallet, API, WALLET_FILE } from "./config.mjs";
11+
import { existsSync } from "fs";
12+
13+
async function setupPaidFetch() {
14+
if (!existsSync(WALLET_FILE)) {
15+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: node wallet.mjs create && node wallet.mjs fund" }));
16+
process.exit(1);
17+
}
18+
const wallet = readWallet();
19+
const { privateKeyToAccount } = await import("viem/accounts");
20+
const { createPublicClient, http } = await import("viem");
21+
const { baseSepolia } = await import("viem/chains");
22+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
23+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
24+
const { toClientEvmSigner } = await import("@x402/evm");
25+
const account = privateKeyToAccount(wallet.privateKey);
26+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
27+
const signer = toClientEvmSigner(account, publicClient);
28+
const client = new x402Client();
29+
client.register("eip155:84532", new ExactEvmScheme(signer));
30+
return wrapFetchWithPayment(fetch, client);
31+
}
32+
33+
async function status() {
34+
const w = readWallet();
35+
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: node wallet.mjs create" })); process.exit(1); }
36+
const { privateKeyToAccount } = await import("viem/accounts");
37+
const account = privateKeyToAccount(w.privateKey);
38+
const timestamp = Math.floor(Date.now() / 1000).toString();
39+
const signature = await account.signMessage({ message: `run402:${timestamp}` });
40+
const res = await fetch(`${API}/tiers/v1/status`, {
41+
headers: { "X-Run402-Wallet": account.address, "X-Run402-Signature": signature, "X-Run402-Timestamp": timestamp },
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 set(tierName) {
49+
if (!tierName) { console.error(JSON.stringify({ status: "error", message: "Usage: node tier.mjs set <prototype|hobby|team>" })); process.exit(1); }
50+
const fetchPaid = await setupPaidFetch();
51+
const res = await fetchPaid(`${API}/tiers/v1/${tierName}`, { method: "POST", headers: { "Content-Type": "application/json" } });
52+
const data = await res.json();
53+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
54+
console.log(JSON.stringify(data, null, 2));
55+
}
56+
57+
const [cmd, ...args] = process.argv.slice(2);
58+
switch (cmd) {
59+
case "status": await status(); break;
60+
case "set": await set(args[0]); break;
61+
default:
62+
console.log("Usage: node tier.mjs <status|set> [args...]");
63+
process.exit(1);
64+
}

openclaw/scripts/wallet.mjs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,6 @@ async function balance() {
105105
}, null, 2));
106106
}
107107

108-
async function tier() {
109-
const w = readWallet();
110-
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: node wallet.mjs create" })); process.exit(1); }
111-
const { privateKeyToAccount } = await loadDeps();
112-
const account = privateKeyToAccount(w.privateKey);
113-
const timestamp = Math.floor(Date.now() / 1000).toString();
114-
const signature = await account.signMessage({ message: `run402:${timestamp}` });
115-
const res = await fetch(`${API}/tiers/v1/status`, {
116-
headers: { "X-Run402-Wallet": account.address, "X-Run402-Signature": signature, "X-Run402-Timestamp": timestamp },
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-
123108
async function exportAddr() {
124109
const w = readWallet();
125110
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet." })); process.exit(1); }
@@ -163,11 +148,10 @@ switch (cmd) {
163148
case "create": await create(); break;
164149
case "fund": await fund(); break;
165150
case "balance": await balance(); break;
166-
case "tier": await tier(); break;
167151
case "export": await exportAddr(); break;
168152
case "checkout": await checkout(args); break;
169153
case "history": await history(args); break;
170154
default:
171-
console.log("Usage: node wallet.mjs <status|create|fund|balance|tier|export|checkout|history>");
155+
console.log("Usage: node wallet.mjs <status|create|fund|balance|export|checkout|history>");
172156
process.exit(1);
173157
}

src/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function formatApiError(
6464
break;
6565
case 403:
6666
lines.push(
67-
`\nNext step: The project lease may have expired. Use \`get_usage\` to check status, or \`renew_project\` to extend the lease.`,
67+
`\nNext step: The project lease may have expired. Use \`get_usage\` to check status, or \`set_tier\` to renew the lease.`,
6868
);
6969
break;
7070
case 404:

0 commit comments

Comments
 (0)