openzoo 0.51.2 → 0.51.4

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/README.md CHANGED
@@ -25,6 +25,8 @@ paid $0.002137 (9.5× cheaper than direct) · rail solana · tx 5Kd…
25
25
 
26
26
  **Claude Code / grokui Auto** — `openzoo claude` sets the Anthropic API key + base URL to the local OpenZoo proxy (x402 pay-per-call). You do **not** need to authenticate Claude first. grokui orange Auto is this harness, not a `RUN:` text parser. PATH `~/.local/bin` is required on Mac so `claude` is found.
27
27
 
28
+ **ChatGPT desktop (official Linux/macOS/Windows app)** — `openzoo chatgpt` puts the zoo in as the Codex provider in `~/.codex/config.toml`, starts a proxy if none is up, and launches the app; choose **sign in with API key** (any key) and the Codex / agent side pays x402 per turn. The plain ChatGPT chat tab still talks to chatgpt.com — it is not API-shaped. On Arch or anything without dpkg, `openzoo chatgpt --deb ~/Downloads/chatgpt_amd64.deb` unpacks the official .deb into `~/.openzoo/apps/chatgpt` and runs it from there (a `chatgpt*.deb` in `~/Downloads` is found automatically). `--no-launch` writes config only; `--print` shows the block.
29
+
28
30
  **Agent of Empires** (tmux session manager for coding agents, [agent-of-empires](https://github.com/agent-of-empires/agent-of-empires)) — `openzoo aoe` registers an `openzoo` agent in aoe's `config.toml` (`[session.custom_agents] openzoo = "openzoo claude"`, detected as `claude` so status hooks, resume and fork all work), starts a background proxy if none is listening (no public tunnel unless `--tunnel`), then runs `aoe`. `openzoo aoe add . -l` creates and launches one session in the current directory; in the TUI pick **openzoo** from the agent picker. `Alt+z` opens a tool session with the wallet balance and the live receipts log. Terminal view only: aoe's structured view runs `claude-agent-acp` itself, which bills Anthropic, not the zoo. `--override-claude` additionally makes aoe's built-in `claude` agent launch through openzoo; `--no-launch` writes config only.
29
31
 
30
32
  **Orca** ([stablyai/orca](https://github.com/stablyai/orca), the desktop orchestrator for parallel agents) — Orca launches any CLI agent in a worktree terminal, so type `openzoo claude` as the agent command (or pick **openzoo** from the agent picker once the upstream catalog entry lands). Orca treats it as Claude Code: same process, same hooks and status dots, same `--dangerously-skip-permissions` default. No account switching applies, because nothing is logged in; every turn pays x402.
package/bin/openzoo.js CHANGED
@@ -146,6 +146,16 @@ usage:
146
146
  voice login / voice watch PREHOOK your own outgoing Telegram
147
147
  messages: type raw, the watcher revises in place (userbot;
148
148
  "." prefix sends raw). X has no edit API — no equivalent.
149
+ npx openzoo chatgpt OpenAI's ChatGPT desktop app on the zoo. Writes the zoo as
150
+ the Codex provider in ~/.codex/config.toml (other tables kept,
151
+ old file backed up), starts a background proxy if none is up,
152
+ and launches the app — the Codex / agent side pays x402 per
153
+ turn; pick "sign in with API key" (any key). The plain chat
154
+ tab still talks to chatgpt.com.
155
+ Arch / no dpkg: --deb ~/Downloads/chatgpt_amd64.deb unpacks it
156
+ (a chatgpt*.deb in ~/Downloads is picked up automatically)
157
+ --model <id> (claude-opus-5) --wire responses|chat --bearer
158
+ --no-launch (config only) --no-proxy --tunnel --print
149
159
  npx openzoo openclaw write the zoo into ~/.openclaw/openclaw.json as a model
150
160
  provider WITH REAL PRICES (OpenClaw's own custom-provider
151
161
  path hard-codes $0.00 and ignores /v1/models pricing)
@@ -170,18 +180,21 @@ usage:
170
180
  npx openzoo balance wallet balance on every rail — Solana (USDC/TOKEN/SOL),
171
181
  Base (USDC/ETH), Robinhood Chain (USDG/memecoins/ETH)
172
182
  npx openzoo build [dir] transmute a Vercel-shaped app (Next.js pages/api,
173
- app/**/route, Vite + api/*) into a Pinocchio Rust
174
- program + asset plan (--out .zoo-out --arch v0|v3)
175
- npx openzoo deploy [dir] deploy the program and the static frontend to
176
- Solana accounts (--cluster mainnet|devnet|localnet,
177
- --yes to accept the rent estimate; burner wallet)
178
- npx openzoo serve <program> local gateway/explorer for a deployed site
179
- (--cluster, --port 4402); /api/* reads are free
183
+ app/**/route, Vite + api/*) for Solana. Default
184
+ --target shared: bytecode for the shared zoo-vm
185
+ runtime, no Rust toolchain, a site is data accounts
186
+ (~0.03-0.1 SOL). --target program: own Pinocchio
187
+ program per site (--arch v0|v3, ~1 SOL+)
188
+ npx openzoo deploy [dir] deploy the site to Solana accounts (--cluster mainnet,
189
+ --runtime <zoo-vm id> or OPENZOO_VM_PROGRAM, --yes to
190
+ accept the rent estimate; burner wallet)
191
+ npx openzoo serve [site] local gateway/explorer for a deployed site (--runtime,
192
+ --cluster, --port 4402); /api/* reads are free
180
193
  simulations, writes are signed by the burner
194
+ npx openzoo runtime deploy deploy the shared zoo-vm runtime once per cluster
181
195
  npx openzoo inspect [dir] print the app in Vercel terms + eligibility report
182
196
  npx openzoo hub hosted explorer for every transmuted site on the
183
197
  cluster (what sites.openzoo.fun runs)
184
- npx openzoo hub hosted explorer for every transmuted site on the cluster
185
198
  npx openzoo address print both funding addresses (Solana + EVM)
186
199
  npx openzoo help this text
187
200
 
@@ -229,6 +242,10 @@ async function main() {
229
242
  // is enough; no patching of the app bundle.
230
243
  await (await import('../lib/grokcli.js')).setupGrokBot(process.argv.slice(3));
231
244
  break;
245
+ case 'chatgpt':
246
+ case 'codex':
247
+ await (await import('../lib/chatgpt.js')).setupChatGpt(process.argv.slice(3));
248
+ break;
232
249
  case 'openclaw':
233
250
  await (await import('../lib/openclaw.js')).setupOpenClaw(process.argv.slice(3));
234
251
  break;
@@ -421,6 +438,7 @@ async function main() {
421
438
  case 'serve':
422
439
  case 'inspect':
423
440
  case 'hub':
441
+ case 'runtime':
424
442
  case 'transmute': {
425
443
  // VERCEL-SHAPED APP -> SOLANA MAINNET. `openzoo build|deploy|serve` hand
426
444
  // the argv to openzoo-transmute (a sibling npm package): it reads the
package/lib/chatgpt.js ADDED
@@ -0,0 +1,328 @@
1
+ /**
2
+ * `npx openzoo chatgpt` — OpenAI's ChatGPT desktop app on the zoo.
3
+ *
4
+ * The Linux preview of the official app (chatgpt.com/download, August 2026)
5
+ * ships the Codex engine inside it: `usr/lib/chatgpt/resources/codex`, and
6
+ * `usr/bin/chatgpt` is a symlink named `codex-launcher`. That engine reads a
7
+ * standard Codex config — `$CODEX_HOME/config.toml`, default `~/.codex` — and
8
+ * its `[model_providers.<id>]` table is the supported way to point it at any
9
+ * OpenAI-shaped server. MEASURED 2026-09-09 on the .deb's `codex` binary:
10
+ * the strings `model_providers`, `env_key`, `requires_openai_auth`,
11
+ * `experimental_bearer_token`, `responses` and `https://api.openai.com/v1`
12
+ * (as a default, next to `ollama` and `amazon-bedrock`) are all there, and
13
+ * a provider block pointed at :8402 answered in the app.
14
+ *
15
+ * What this does and does not redirect, so nobody is surprised:
16
+ * - the Codex / agent side of the app ("sign in with API key") → the zoo,
17
+ * paid per turn by x402 through the local proxy;
18
+ * - the plain ChatGPT conversation tab → still chatgpt.com. It talks to
19
+ * `chatgpt_base_url = "https://chatgpt.com/backend-api/"`, a private
20
+ * backend that is not API-shaped, so there is nothing to point at us.
21
+ * - the two auth modes are exclusive: the binary logs "ChatGPT login is
22
+ * required, but an API key is currently being used. Logging out." Pick
23
+ * API-key mode for the session that should pay x402.
24
+ *
25
+ * One command, end to end: write the provider into config.toml (backing up
26
+ * the old file, keeping every other table), make sure a proxy is listening,
27
+ * find the app — or unpack the downloaded .deb on a distro that cannot
28
+ * install it, which is how this was first done on Arch — and launch it with
29
+ * the env var the provider's `env_key` names.
30
+ */
31
+ import { spawn, spawnSync } from 'node:child_process';
32
+ import fs from 'node:fs';
33
+ import os from 'node:os';
34
+ import path from 'node:path';
35
+ import { config } from './config.js';
36
+
37
+ export const PROVIDER_ID = 'openzoo';
38
+ export const API_KEY_ENV = 'OPENZOO_API_KEY';
39
+ export const API_KEY_VALUE = 'sk-openzoo'; // any value: the zoo takes payment, not keys
40
+
41
+ /** Codex's home: $CODEX_HOME, else ~/.codex. */
42
+ export function codexHome(env = process.env, home = os.homedir()) {
43
+ return env.CODEX_HOME || path.join(home, '.codex');
44
+ }
45
+
46
+ export function codexConfigPath(env = process.env, home = os.homedir()) {
47
+ return path.join(codexHome(env, home), 'config.toml');
48
+ }
49
+
50
+ export function defaultModel(env = process.env) {
51
+ // Bare ids are what the doors serve; the gateway resolves the rest.
52
+ return env.OPENZOO_DEFAULT_MODEL || 'claude-opus-5';
53
+ }
54
+
55
+ /**
56
+ * The provider table Codex reads. `wire_api = "responses"` because the local
57
+ * proxy forwards POST /v1/responses as a paid path (lib/proxy.js) and that is
58
+ * what Codex speaks natively; `--wire chat` is there for a build that only
59
+ * does chat completions. `bearer` writes a static token instead of an env
60
+ * key, for launches this command does not control (a desktop launcher).
61
+ */
62
+ export function providerBlock({ port = config.port, wireApi = 'responses', bearer = false } = {}) {
63
+ const lines = [
64
+ `[model_providers.${PROVIDER_ID}]`,
65
+ `name = "${PROVIDER_ID}"`,
66
+ `base_url = "http://localhost:${port}/v1"`,
67
+ `wire_api = "${wireApi}"`,
68
+ bearer ? `experimental_bearer_token = "${API_KEY_VALUE}"` : `env_key = "${API_KEY_ENV}"`,
69
+ 'requires_openai_auth = false',
70
+ ];
71
+ return `${lines.join('\n')}\n`;
72
+ }
73
+
74
+ const isTableHeader = (line) => /^\s*\[/.test(line);
75
+ const OUR_TABLE = new RegExp(`^\\s*\\[model_providers\\.${PROVIDER_ID}\\]\\s*$`);
76
+ const OUR_MARK = '# written by `npx openzoo chatgpt` — rerun it to refresh, edit freely otherwise';
77
+
78
+ /**
79
+ * Merge our provider into an existing config.toml TEXT. Pure.
80
+ *
81
+ * TOML puts top-level keys before the first table, so `model` and
82
+ * `model_provider` are (re)written at the very top and any earlier copies of
83
+ * those two keys in the top-level span are dropped. Our own table is replaced
84
+ * wholesale — it is generated, and a stale half-merge is worse than none.
85
+ * Every other line, table and comment is preserved byte-for-byte. Running
86
+ * this twice yields identical text.
87
+ */
88
+ export function mergeCodexConfig(existing, { model, port, wireApi, bearer } = {}) {
89
+ const src = String(existing || '').replace(/\r\n/g, '\n');
90
+ const lines = src === '' ? [] : src.split('\n');
91
+
92
+ // 1. drop our previous table (header through the line before the next header)
93
+ const kept = [];
94
+ let skipping = false;
95
+ for (const line of lines) {
96
+ if (OUR_TABLE.test(line)) { skipping = true; continue; }
97
+ if (skipping && isTableHeader(line)) skipping = false;
98
+ if (skipping) continue;
99
+ if (line.trim() === OUR_MARK) continue;
100
+ kept.push(line);
101
+ }
102
+
103
+ // 2. split into the top-level span and the tables that follow
104
+ const firstTable = kept.findIndex(isTableHeader);
105
+ const top = firstTable === -1 ? kept : kept.slice(0, firstTable);
106
+ const tables = firstTable === -1 ? [] : kept.slice(firstTable);
107
+
108
+ // 3. our two keys lead; earlier copies go
109
+ const ours = new Set(['model', 'model_provider']);
110
+ const topRest = top.filter((line) => {
111
+ const m = /^\s*([A-Za-z0-9_-]+)\s*=/.exec(line);
112
+ return !(m && ours.has(m[1]));
113
+ });
114
+ const head = [
115
+ OUR_MARK,
116
+ `model = "${model || defaultModel()}"`,
117
+ `model_provider = "${PROVIDER_ID}"`,
118
+ ];
119
+ // trim blank padding so reruns do not accumulate empty lines
120
+ const trimBlank = (arr) => {
121
+ const out = [...arr];
122
+ while (out.length && out[0].trim() === '') out.shift();
123
+ while (out.length && out[out.length - 1].trim() === '') out.pop();
124
+ return out;
125
+ };
126
+ const parts = [head.join('\n')];
127
+ const restTop = trimBlank(topRest);
128
+ if (restTop.length) parts.push(restTop.join('\n'));
129
+ const restTables = trimBlank(tables);
130
+ if (restTables.length) parts.push(restTables.join('\n'));
131
+ parts.push(providerBlock({ port, wireApi, bearer }).trimEnd());
132
+ return `${parts.join('\n\n')}\n`;
133
+ }
134
+
135
+ /** Candidate app binaries, most specific first. Exported for tests. */
136
+ export function chatGptCandidates({ env = process.env, home = os.homedir(), platform = process.platform } = {}) {
137
+ const out = [];
138
+ if (env.OPENZOO_CHATGPT_BIN) out.push(env.OPENZOO_CHATGPT_BIN);
139
+ if (platform === 'darwin') {
140
+ out.push('/Applications/ChatGPT.app/Contents/MacOS/ChatGPT');
141
+ out.push(path.join(home, 'Applications', 'ChatGPT.app', 'Contents', 'MacOS', 'ChatGPT'));
142
+ } else if (platform === 'win32') {
143
+ const local = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
144
+ out.push(path.join(local, 'Programs', 'ChatGPT', 'ChatGPT.exe'));
145
+ } else {
146
+ // the .deb's layout (apt install), then where THIS command unpacks it,
147
+ // then the by-hand unpack path the first Arch run used
148
+ out.push('/usr/lib/chatgpt/ChatGPT');
149
+ out.push('/opt/ChatGPT/chatgpt');
150
+ out.push(path.join(unpackDir(home), 'usr', 'lib', 'chatgpt', 'ChatGPT'));
151
+ out.push(path.join(home, 'apps', 'chatgpt', 'usr', 'lib', 'chatgpt', 'ChatGPT'));
152
+ }
153
+ for (const dir of String(env.PATH || '').split(path.delimiter).filter(Boolean)) {
154
+ out.push(path.join(dir, platform === 'win32' ? 'chatgpt.exe' : 'chatgpt'));
155
+ }
156
+ return out;
157
+ }
158
+
159
+ export function resolveChatGptApp(opts = {}) {
160
+ const exists = opts.exists || ((p) => { try { fs.accessSync(p, fs.constants.X_OK); return true; } catch { return false; } });
161
+ return chatGptCandidates(opts).find(exists) || null;
162
+ }
163
+
164
+ /** Where `--deb` (or an auto-found download) is unpacked. */
165
+ export function unpackDir(home = os.homedir()) {
166
+ return path.join(home, '.openzoo', 'apps', 'chatgpt');
167
+ }
168
+
169
+ /** Newest chatgpt*.deb in ~/Downloads, or null. */
170
+ export function findDownloadedDeb(home = os.homedir()) {
171
+ const dir = path.join(home, 'Downloads');
172
+ let names;
173
+ try { names = fs.readdirSync(dir); } catch { return null; }
174
+ const debs = names
175
+ .filter((n) => /^chatgpt.*\.deb$/i.test(n))
176
+ .map((n) => path.join(dir, n))
177
+ .map((p) => ({ p, t: fs.statSync(p).mtimeMs }))
178
+ .sort((a, b) => b.t - a.t);
179
+ return debs.length ? debs[0].p : null;
180
+ }
181
+
182
+ function have(bin) {
183
+ return spawnSync(bin, ['--version'], { stdio: 'ignore' }).status !== null;
184
+ }
185
+
186
+ /**
187
+ * Unpack a .deb without dpkg: a .deb is an `ar` archive holding data.tar.*.
188
+ * bsdtar reads both layers; `ar` + `tar` is the fallback. Returns the app
189
+ * binary path inside `dest`.
190
+ */
191
+ export function unpackDeb(deb, dest = unpackDir()) {
192
+ if (!fs.existsSync(deb)) throw new Error(`no such file: ${deb}`);
193
+ fs.rmSync(dest, { recursive: true, force: true });
194
+ fs.mkdirSync(dest, { recursive: true });
195
+ const outer = fs.mkdtempSync(path.join(os.tmpdir(), 'openzoo-deb-'));
196
+ const run = (cmd, args, cwd) => {
197
+ const r = spawnSync(cmd, args, { cwd, stdio: ['ignore', 'ignore', 'pipe'] });
198
+ if (r.status !== 0) throw new Error(`${cmd} ${args.join(' ')} failed: ${String(r.stderr || '').trim().slice(0, 200)}`);
199
+ };
200
+ if (have('bsdtar')) run('bsdtar', ['-xf', deb], outer);
201
+ else if (have('ar')) run('ar', ['x', deb], outer);
202
+ else throw new Error('need bsdtar (libarchive) or ar (binutils) to unpack a .deb');
203
+ const data = fs.readdirSync(outer).find((n) => /^data\.tar/.test(n));
204
+ if (!data) throw new Error('not a .deb: no data.tar inside');
205
+ run('tar', ['-xf', path.join(outer, data), '-C', dest]);
206
+ fs.rmSync(outer, { recursive: true, force: true });
207
+ const app = path.join(dest, 'usr', 'lib', 'chatgpt', 'ChatGPT');
208
+ if (!fs.existsSync(app)) throw new Error(`unpacked, but ${app} is not there — not the ChatGPT .deb?`);
209
+ return app;
210
+ }
211
+
212
+ async function ensureProxy({ tunnel }) {
213
+ const { startProxy, oursOn, packageVersion } = await import('./proxy.js');
214
+ if (await oursOn(config.port)) return { started: false };
215
+ // DETACHED, like `openzoo aoe`: the app outlives this command, so the
216
+ // proxy must too. No public tunnel unless asked — a detached proxy nobody
217
+ // watches must not open a URL + key that spends the wallet.
218
+ const logPath = path.join(os.homedir(), '.openzoo', 'proxy.log');
219
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
220
+ const fd = fs.openSync(logPath, 'a');
221
+ const env = { ...process.env };
222
+ if (!tunnel && !env.OPENZOO_NO_TUNNEL) env.OPENZOO_NO_TUNNEL = '1';
223
+ const child = spawn(process.execPath, [process.argv[1], 'proxy'], { detached: true, stdio: ['ignore', fd, fd], env });
224
+ child.unref();
225
+ fs.closeSync(fd);
226
+ process.stderr.write(`openzoo: starting the proxy (v${packageVersion()}) in the background, log ${logPath}`);
227
+ const t0 = Date.now();
228
+ while (Date.now() - t0 < 60_000) {
229
+ await new Promise((r) => setTimeout(r, 1000));
230
+ process.stderr.write('.');
231
+ try {
232
+ const r = await fetch(`http://localhost:${config.port}/v1/info`);
233
+ if (r.ok) { process.stderr.write('\n'); return { started: true, logPath }; }
234
+ } catch { /* not yet */ }
235
+ }
236
+ process.stderr.write('\n');
237
+ throw new Error(`proxy did not answer on :${config.port} within 60s — see ${logPath}`);
238
+ }
239
+
240
+ const INSTALL_HELP = `openzoo: ChatGPT desktop app not found.
241
+ download it from https://chatgpt.com/download (Linux: the .deb; macOS/Windows: the installer), then:
242
+ Debian/Ubuntu: sudo apt install ./chatgpt_amd64.deb
243
+ Fedora: sudo dnf install ./chatgpt*.rpm
244
+ Arch / other: npx openzoo chatgpt --deb ~/Downloads/chatgpt_amd64.deb (unpacked, no dpkg needed)
245
+ or point OPENZOO_CHATGPT_BIN at the binary.`;
246
+
247
+ /**
248
+ * `npx openzoo chatgpt [--model <id>] [--deb <file>] [--wire responses|chat]
249
+ * [--bearer] [--no-launch] [--no-proxy] [--tunnel] [--print]`
250
+ */
251
+ export async function setupChatGpt(argv = []) {
252
+ const args = [...argv];
253
+ const opt = { model: null, deb: null, wireApi: 'responses', bearer: false, launch: true, proxy: true, tunnel: false, print: false, configPath: null, rest: [] };
254
+ while (args.length) {
255
+ const a = args.shift();
256
+ if (a === '--model') opt.model = String(args.shift() || '').trim() || null;
257
+ else if (a === '--deb') opt.deb = String(args.shift() || '').trim() || null;
258
+ else if (a === '--wire') opt.wireApi = String(args.shift() || '').trim();
259
+ else if (a === '--config') opt.configPath = String(args.shift() || '').trim() || null;
260
+ else if (a === '--bearer') opt.bearer = true;
261
+ else if (a === '--no-launch') opt.launch = false;
262
+ else if (a === '--no-proxy') opt.proxy = false;
263
+ else if (a === '--tunnel') opt.tunnel = true;
264
+ else if (a === '--print') opt.print = true;
265
+ else if (a.startsWith('--')) throw new Error(`unknown flag: ${a}`);
266
+ else opt.rest.push(a);
267
+ }
268
+ if (!['responses', 'chat'].includes(opt.wireApi)) throw new Error('--wire must be responses or chat');
269
+ const model = opt.model || defaultModel();
270
+ const merge = { model, port: config.port, wireApi: opt.wireApi, bearer: opt.bearer };
271
+
272
+ if (opt.print) {
273
+ process.stdout.write(mergeCodexConfig('', merge));
274
+ return { printed: true };
275
+ }
276
+
277
+ // 1. config.toml
278
+ const file = opt.configPath || codexConfigPath();
279
+ let existing = '';
280
+ if (fs.existsSync(file)) {
281
+ existing = fs.readFileSync(file, 'utf8');
282
+ fs.copyFileSync(file, `${file}.openzoo-backup`);
283
+ } else {
284
+ fs.mkdirSync(path.dirname(file), { recursive: true });
285
+ }
286
+ const next = mergeCodexConfig(existing, merge);
287
+ if (next !== existing) fs.writeFileSync(file, next);
288
+ console.log(`${next !== existing ? 'wrote' : 'kept'} ${file}: model_provider = "${PROVIDER_ID}" (http://localhost:${config.port}/v1, ${opt.wireApi}), model = "${model}"`);
289
+
290
+ // 2. proxy
291
+ if (opt.proxy) {
292
+ const p = await ensureProxy({ tunnel: opt.tunnel });
293
+ console.log(p.started ? `proxy up on :${config.port} (background)` : `proxy already on :${config.port}`);
294
+ }
295
+
296
+ if (!opt.launch) {
297
+ console.log(`config only. launch the app with ${API_KEY_ENV}=${API_KEY_VALUE} in its environment and pick "sign in with API key".`);
298
+ return { file, model, launched: false };
299
+ }
300
+
301
+ // 3. the app
302
+ let app = resolveChatGptApp();
303
+ if (!app && process.platform === 'linux') {
304
+ const deb = opt.deb || findDownloadedDeb();
305
+ if (deb) {
306
+ console.log(`unpacking ${deb} into ${unpackDir()} (no dpkg needed)`);
307
+ app = unpackDeb(deb);
308
+ }
309
+ } else if (opt.deb) {
310
+ app = unpackDeb(opt.deb);
311
+ }
312
+ if (!app) {
313
+ console.error(INSTALL_HELP);
314
+ process.exitCode = 1;
315
+ return { file, model, launched: false };
316
+ }
317
+
318
+ // 4. launch, with the env var the provider's env_key names
319
+ const env = { ...process.env, [API_KEY_ENV]: process.env[API_KEY_ENV] || API_KEY_VALUE };
320
+ if (!env.CODEX_HOME) env.CODEX_HOME = codexHome();
321
+ console.log(`launching ${app}`);
322
+ console.log(` Codex / agent mode → the zoo, paid per turn by x402. Choose "sign in with API key" (any key).`);
323
+ console.log(' the plain ChatGPT chat tab still talks to chatgpt.com — that half is not API-shaped.');
324
+ const child = spawn(app, opt.rest, { stdio: 'ignore', env, detached: true });
325
+ child.on('error', (e) => console.error(`openzoo: could not launch ${app}: ${e.message}`));
326
+ child.unref();
327
+ return { file, model, app, launched: true };
328
+ }
package/lib/proxy.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  } from './config.js';
10
10
  import { execSync } from 'node:child_process';
11
11
  import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
12
- import { withOnrampLink, settleFailCopy, isFundInstruction } from './stripeOnramp.js';
12
+ import { withOnrampLink, settleFailCopy } from './stripeOnramp.js';
13
13
  import { tokenBalance } from './x402.js';
14
14
  import { evmTokenBalance } from './evm.js';
15
15
  import { autoContext } from './autobind.js';
@@ -1023,11 +1023,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1023
1023
  // gateway itself named insufficient_funds. UnderfundedError (empty
1024
1024
  // wallet preflight) is handled in the catch below.
1025
1025
  let msg = copy.message;
1026
- const wantOnramp = copy.code === 'insufficient_funds'
1027
- || (!paid && isFundInstruction(copy.reason, copy));
1028
- if (wantOnramp) {
1029
- msg = await withOnrampLink(msg, { solana: client.address, usd, code: copy.code });
1030
- }
1026
+ // EVERY PAYMENT FAILURE ENDS WITH HOW TO PAY. A caller staring at a
1027
+ // facilitator's wording ("execution reverted", "authorization value
1028
+ // mismatch") has exactly three moves — card via Whop, USDC on Solana,
1029
+ // USDC on Base — so every 402 after a payment carries all three, not
1030
+ // only the ones the gateway managed to classify as insufficient funds.
1031
+ msg = await withOnrampLink(msg, { solana: client.address, evm: client.evmAddress, usd, code: copy.code, force: true });
1031
1032
  log(/ties to your account/i.test(msg) ? 'onramp: whop + copy-paste solana' : `402: ${msg.slice(0, 140)}`);
1032
1033
  jsonErr(res, paid ? copy.status : 402, msg);
1033
1034
  return;
@@ -1039,7 +1040,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1039
1040
  jsonErr(res, 402, err.message, { quote: err.quote });
1040
1041
  } else if (err instanceof UnderfundedError) {
1041
1042
  const msg = await withOnrampLink(err.message, {
1042
- solana: client.address, usd: Number(err.accept?.extra?.billedUsd),
1043
+ solana: client.address, evm: client.evmAddress, usd: Number(err.accept?.extra?.billedUsd),
1043
1044
  });
1044
1045
  log(msg);
1045
1046
  jsonErr(res, 402, msg);
@@ -91,15 +91,21 @@ export function whopBuyUrl() {
91
91
  return String(process.env.OPENZOO_WHOP_CHECKOUT || process.env.OPENZOO_WHOP_URL || DEFAULT_WHOP_CHECKOUT).trim();
92
92
  }
93
93
 
94
- export function whopFundBlurb(solana) {
94
+ export function whopFundBlurb(solana, evm) {
95
95
  const addr = String(solana || '').trim();
96
96
  if (!addr) return '';
97
97
  const url = whopBuyUrl();
98
+ const base = String(evm || '').trim();
98
99
  return [
99
100
  `Hey — buy this: ${url}`,
100
101
  '',
101
102
  'Copy-paste THIS Solana address into "what is your Solana address?" so it ties to your account:',
102
103
  addr,
104
+ // OR FUND THE WALLET DIRECTLY. Every failed payment ends here: card via
105
+ // Whop, or USDC to either address — those are the only three moves a
106
+ // stuck caller has, so they are the whole message.
107
+ '',
108
+ `Or send USDC directly — Solana: ${addr}${base ? ` · Base: ${base}` : ''}`,
103
109
  ].join('\n');
104
110
  }
105
111
 
@@ -154,8 +160,8 @@ export function settleFailCopy(q402) {
154
160
 
155
161
  export async function withOnrampLink(text, dest) {
156
162
  const body = String(text || '').trim();
157
- if (!isFundInstruction(body, dest)) return body;
158
- const blurb = whopFundBlurb(dest?.solana);
163
+ if (!dest?.force && !isFundInstruction(body, dest)) return body;
164
+ const blurb = whopFundBlurb(dest?.solana, dest?.evm);
159
165
  if (!blurb) return body;
160
166
  if (/ties to your account/i.test(body) && body.includes(String(dest.solana))) return body;
161
167
  return `${blurb}\n\n${body}`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.51.2",
4
- "description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental. `openzoo build|deploy|serve|hub` transmute a Vercel-shaped app (Next.js / Vite + /api) into a Pinocchio program + asset accounts on Solana mainnet.",
3
+ "version": "0.51.4",
4
+ "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental. `openzoo build|deploy|serve|hub|runtime` transmute a Vercel-shaped app (Next.js / Vite + /api) into Solana accounts on the shared zoo-vm Pinocchio runtime (~0.05 SOL a site) or a dedicated program, mainnet.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -63,17 +63,48 @@ Requirements: Node ≥ 18, and for `build`/`deploy` the Solana toolchain
63
63
  (`cargo build-sbf`): `sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"`.
64
64
  `build --skip-cargo` works without it (you get the crate and the cost sheet, no `.so`).
65
65
 
66
- ## Quickstart
66
+ ## Quickstart (shared runtime — the default)
67
+
68
+ A site does not need its own program. The `zoo-vm` runtime
69
+ (`runtime/zoo-vm`, prebuilt at `prebuilt/zoo_vm.v0.so`) is deployed once per
70
+ cluster; every site is then just data accounts under it — a 66-byte site
71
+ account, its bytecode module (`/.zoo/code.bin`, ~1–2 KB for a handful of
72
+ routes), its static files and its manifest. Rent for a small site is
73
+ **≈0.03–0.1 SOL** instead of ≈1 SOL+ for a dedicated program.
67
74
 
68
75
  ```sh
69
76
  cd my-next-app # or a Vite repo with api/*.js, or a .vercel/output
70
77
  npx openzoo-transmute inspect # the app in Vercel terms + eligibility report
71
- npx openzoo-transmute build # → .zoo-out/.zoo/{crate,manifest.json,deploy/*.so}
72
- npx openzoo-transmute deploy --cluster devnet
73
- npx openzoo-transmute serve <programId> --cluster devnet
78
+ npx openzoo-transmute build # → .zoo-out/.zoo/{code.bin,manifest.json,…}; no cargo needed
79
+ npx openzoo-transmute deploy --cluster mainnet --runtime <zoo-vm id> --yes
80
+ npx openzoo-transmute serve # site id + runtime read from .zoo/deploy.json
74
81
  open http://127.0.0.1:4402/ # explorer at /.zoo/, manifest at /.zoo/manifest.json
75
82
  ```
76
83
 
84
+ The site id is the public key of `.zoo/site-keypair.json` (looks like a
85
+ program id, costs nothing; keep it — redeploys update the same site). The hub
86
+ serves it at `https://sites.openzoo.fun/s/<siteId>` once its
87
+ `OPENZOO_VM_PROGRAM` points at the same runtime.
88
+
89
+ Deploying the runtime itself (once, ≈2.2 SOL on mainnet for the 313 KB .so,
90
+ upgradeable by the deployer's key):
91
+
92
+ ```sh
93
+ npx openzoo-transmute runtime deploy --cluster mainnet --yes # keypair → ./zoo-vm-keypair.json
94
+ export OPENZOO_VM_PROGRAM=<printed id>
95
+ ```
96
+
97
+ `--target program` keeps the original path: one Pinocchio program per site,
98
+ compiled from the generated crate with `cargo build-sbf`.
99
+
100
+ ## Quickstart (dedicated program)
101
+
102
+ ```sh
103
+ npx openzoo-transmute build --target program # → .zoo-out/.zoo/{crate,manifest.json,deploy/*.so}
104
+ npx openzoo-transmute deploy --cluster mainnet --yes
105
+ npx openzoo-transmute serve <programId> --cluster mainnet
106
+ ```
107
+
77
108
  `inspect` prints one line per function with its route, style, runtime,
78
109
  `maxDuration`, `memory` and methods, marks each `✓`/`✗` and says why:
79
110
 
@@ -115,14 +146,16 @@ POST /api/counter → 402 payment required (gateway started without
115
146
 
116
147
  | command | what it does |
117
148
  |---|---|
118
- | `build [dir] [--out .zoo-out] [--name <crate>] [--arch v0\|v3] [--cluster <c>] [--skip-cargo] [--json]` | read the app (`lib/vercel.js`), transmute (`lib/compile/`), write `<out>/.zoo/{crate/, manifest.json, report.json, static-plan.json, build.json}`, run `cargo build-sbf --arch <arch>` → `.zoo/deploy/<name>.so`, print the cost sheet (`--cluster` only chooses the RPC whose rent parameters price it, default `localnet`; unreachable → the 6.96 SOL/MB rule). Default arch is `v0` (mainnet); `deploy` re-detects the cluster's SBPF version and rebuilds if needed. Exit code 2 when nothing was eligible |
119
- | `deploy [dir\|outDir] [--cluster mainnet\|devnet\|testnet\|localnet\|<url>] [--keypair <path>] [--yes] [--program <id>] [--concurrency 4] [--skip-assets] [--force] [--json]` | deploy or upgrade the program (keypair kept at `.zoo/program-keypair.json`, so redeploys upgrade in place), upload every static file (unchanged ones skipped by comparing bytes on chain), write `/.zoo/manifest.json`, record `.zoo/deploy.json` |
120
- | `serve [programId] [--cluster <c>] [--port 4402] [--host 127.0.0.1] [--keypair <path>\|--no-keypair] [--quiet]` | the local gateway; `programId` defaults to the `.zoo/deploy.json` under the current directory (or its `.zoo-out/`). Reads are simulated, writes are signed by the wallet; without a wallet (or with `--no-keypair`) writes answer 402. Without `--cluster`/`OPENZOO_CLUSTER` this command assumes `localnet`, unlike the others |
149
+ | `build [dir] [--target shared\|program] [--runtime <id>] [--out .zoo-out] [--name <crate>] [--arch v0\|v3] [--cluster <c>] [--skip-cargo] [--json]` | `--target shared` (default): transmute to a `ZOOB` bytecode module at `<out>/.zoo/code.bin` for the shared runtime, no cargo, site-sized cost sheet. `--target program`: read the app (`lib/vercel.js`), transmute (`lib/compile/`), write `<out>/.zoo/{crate/, manifest.json, report.json, static-plan.json, build.json}`, run `cargo build-sbf --arch <arch>` → `.zoo/deploy/<name>.so`, print the cost sheet (`--cluster` only chooses the RPC whose rent parameters price it, default `localnet`; unreachable → the 6.96 SOL/MB rule). Default arch is `v0` (mainnet); `deploy` re-detects the cluster's SBPF version and rebuilds if needed. Exit code 2 when nothing was eligible |
150
+ | `deploy [dir\|outDir] [--cluster mainnet\|devnet\|testnet\|localnet\|<url>] [--keypair <path>] [--yes] [--runtime <id>] [--program <id>] [--concurrency 4] [--skip-assets] [--force] [--json]` | shared build: create the site account under `--runtime` (or `OPENZOO_VM_PROGRAM`), upload `code.bin`, assets and manifest as PDAs namespaced by the site id (keypair at `.zoo/site-keypair.json`). Program build: deploy or upgrade the program (keypair kept at `.zoo/program-keypair.json`, so redeploys upgrade in place), upload every static file (unchanged ones skipped by comparing bytes on chain), write `/.zoo/manifest.json`, record `.zoo/deploy.json` |
151
+ | `serve [siteId\|programId] [--runtime <id>] [--cluster <c>] [--port 4402] [--host 127.0.0.1] [--keypair <path>\|--no-keypair] [--quiet]` | the local gateway; with `--runtime` the positional is a site id on the shared runtime. It defaults to the `.zoo/deploy.json` under the current directory (or its `.zoo-out/`). Reads are simulated, writes are signed by the wallet; without a wallet (or with `--no-keypair`) writes answer 402. Without `--cluster`/`OPENZOO_CLUSTER` this command assumes `localnet`, unlike the others |
121
152
  | `inspect [dir] [--json]` | the Vercel model (functions, static files, routes, crons) plus the eligibility report |
122
- | `status <programId> [--cluster <c>] [--json]` | program account, authority, `maxDataLen`, deploy slot, and the on-chain manifest |
153
+ | `status <siteId\|programId> [--runtime <id>] [--cluster <c>] [--json]` | program account, authority, `maxDataLen`, deploy slot, and the on-chain manifest; with `--runtime`, the site account and its manifest |
154
+ | `runtime deploy [--cluster <c>] [--keypair <path>] [--yes] [--program-keypair zoo-vm-keypair.json] [--so <path>]` | deploy (or upgrade in place) the shared `zoo-vm` runtime: `prebuilt/zoo_vm.<arch>.so`, or built from `runtime/zoo-vm` when no prebuilt matches the cluster's SBPF version |
155
+ | `runtime status <id> [--cluster <c>]` | the runtime program's account |
123
156
  | `help`, `--version` | |
124
157
 
125
- Environment: `OPENZOO_CLUSTER` (default `mainnet` for `deploy`/`status`, `localnet` for `serve`), `OPENZOO_RPC` (mainnet RPC
158
+ Environment: `OPENZOO_VM_PROGRAM` (shared runtime id; default for `--runtime`), `OPENZOO_TARGET` (`shared`/`program`), `OPENZOO_CLUSTER` (default `mainnet` for `deploy`/`status`, `localnet` for `serve`), `OPENZOO_RPC` (mainnet RPC
126
159
  URL), `OPENZOO_KEYPAIR` / `OPENZOO_WALLET` (signer path), `OPENZOO_DEBUG=1`
127
160
  (stack traces). Signer discovery order (`lib/wallet.js`): `--keypair` →
128
161
  `OPENZOO_KEYPAIR` → `OPENZOO_WALLET` or `~/.openzoo/wallet.json` (the openzoo