openzoo 0.51.3 → 0.51.5
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 +2 -0
- package/bin/openzoo.js +14 -37
- package/lib/chatgpt.js +328 -0
- package/lib/config.js +34 -0
- package/lib/pay.js +60 -4
- package/lib/proxy.js +8 -7
- package/lib/stripeOnramp.js +9 -3
- package/package.json +4 -7
- package/transmute/README.md +0 -385
- package/transmute/bin/openzoo-transmute.js +0 -7
- package/transmute/docs/OPENZOO_CLI_PATCH.md +0 -139
- package/transmute/docs/VERCEL_TO_SOLANA.md +0 -381
- package/transmute/lib/build.js +0 -381
- package/transmute/lib/cli.js +0 -293
- package/transmute/lib/compile/bytecode.js +0 -591
- package/transmute/lib/compile/hostids.js +0 -11
- package/transmute/lib/compile/index.js +0 -159
- package/transmute/lib/compile/ir.js +0 -1634
- package/transmute/lib/compile/parse.js +0 -753
- package/transmute/lib/compile/rust.js +0 -781
- package/transmute/lib/deploy.js +0 -357
- package/transmute/lib/eligibility.js +0 -211
- package/transmute/lib/gateway.js +0 -710
- package/transmute/lib/hub.js +0 -290
- package/transmute/lib/solana.js +0 -387
- package/transmute/lib/vercel.js +0 -296
- package/transmute/lib/wallet.js +0 -41
- package/transmute/lib/wire.js +0 -208
- package/transmute/prebuilt/zoo_vm.v0.so +0 -0
- package/transmute/runtime/zoo-host/Cargo.lock +0 -588
- package/transmute/runtime/zoo-host/Cargo.toml +0 -30
- package/transmute/runtime/zoo-host/examples/vmrun.rs +0 -43
- package/transmute/runtime/zoo-host/src/assets.rs +0 -214
- package/transmute/runtime/zoo-host/src/ctx.rs +0 -350
- package/transmute/runtime/zoo-host/src/fmt.rs +0 -188
- package/transmute/runtime/zoo-host/src/helpers.rs +0 -245
- package/transmute/runtime/zoo-host/src/json.rs +0 -263
- package/transmute/runtime/zoo-host/src/kv.rs +0 -206
- package/transmute/runtime/zoo-host/src/lib.rs +0 -137
- package/transmute/runtime/zoo-host/src/site.rs +0 -94
- package/transmute/runtime/zoo-host/src/val.rs +0 -1050
- package/transmute/runtime/zoo-host/src/vm.rs +0 -902
- package/transmute/runtime/zoo-host/src/wire.rs +0 -239
- package/transmute/runtime/zoo-vm/Cargo.lock +0 -596
- package/transmute/runtime/zoo-vm/Cargo.toml +0 -23
- package/transmute/runtime/zoo-vm/src/lib.rs +0 -92
- package/transmute/site/.env.production +0 -2
- package/transmute/site/api/echo.js +0 -10
- package/transmute/site/api/hello.js +0 -12
- package/transmute/site/api/hits.js +0 -13
- package/transmute/site/api/time.js +0 -5
- package/transmute/site/package.json +0 -6
- package/transmute/site/public/app.js +0 -16
- package/transmute/site/public/index.html +0 -96
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)
|
|
@@ -169,22 +179,6 @@ usage:
|
|
|
169
179
|
npx openzoo contexts --forget <hash|all> drop manifest entries
|
|
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
|
-
npx openzoo build [dir] transmute a Vercel-shaped app (Next.js pages/api,
|
|
173
|
-
app/**/route, Vite + api/*) for Solana. Default
|
|
174
|
-
--target shared: bytecode for the shared zoo-vm
|
|
175
|
-
runtime, no Rust toolchain, a site is data accounts
|
|
176
|
-
(~0.03-0.1 SOL). --target program: own Pinocchio
|
|
177
|
-
program per site (--arch v0|v3, ~1 SOL+)
|
|
178
|
-
npx openzoo deploy [dir] deploy the site to Solana accounts (--cluster mainnet,
|
|
179
|
-
--runtime <zoo-vm id> or OPENZOO_VM_PROGRAM, --yes to
|
|
180
|
-
accept the rent estimate; burner wallet)
|
|
181
|
-
npx openzoo serve [site] local gateway/explorer for a deployed site (--runtime,
|
|
182
|
-
--cluster, --port 4402); /api/* reads are free
|
|
183
|
-
simulations, writes are signed by the burner
|
|
184
|
-
npx openzoo runtime deploy deploy the shared zoo-vm runtime once per cluster
|
|
185
|
-
npx openzoo inspect [dir] print the app in Vercel terms + eligibility report
|
|
186
|
-
npx openzoo hub hosted explorer for every transmuted site on the
|
|
187
|
-
cluster (what sites.openzoo.fun runs)
|
|
188
182
|
npx openzoo address print both funding addresses (Solana + EVM)
|
|
189
183
|
npx openzoo help this text
|
|
190
184
|
|
|
@@ -232,6 +226,10 @@ async function main() {
|
|
|
232
226
|
// is enough; no patching of the app bundle.
|
|
233
227
|
await (await import('../lib/grokcli.js')).setupGrokBot(process.argv.slice(3));
|
|
234
228
|
break;
|
|
229
|
+
case 'chatgpt':
|
|
230
|
+
case 'codex':
|
|
231
|
+
await (await import('../lib/chatgpt.js')).setupChatGpt(process.argv.slice(3));
|
|
232
|
+
break;
|
|
235
233
|
case 'openclaw':
|
|
236
234
|
await (await import('../lib/openclaw.js')).setupOpenClaw(process.argv.slice(3));
|
|
237
235
|
break;
|
|
@@ -419,27 +417,6 @@ async function main() {
|
|
|
419
417
|
if (bal <= 0) console.log('buy some with: npx openzoo topup 10');
|
|
420
418
|
break;
|
|
421
419
|
}
|
|
422
|
-
case 'build':
|
|
423
|
-
case 'deploy':
|
|
424
|
-
case 'serve':
|
|
425
|
-
case 'inspect':
|
|
426
|
-
case 'hub':
|
|
427
|
-
case 'runtime':
|
|
428
|
-
case 'transmute': {
|
|
429
|
-
// VERCEL-SHAPED APP -> SOLANA MAINNET. `openzoo build|deploy|serve` hand
|
|
430
|
-
// the argv to openzoo-transmute (a sibling npm package): it reads the
|
|
431
|
-
// app the way `vercel build` would (pages/api, app/**/route, api/*,
|
|
432
|
-
// .vercel/output), transmutes each Lambda into a Pinocchio Rust program
|
|
433
|
-
// route, stores the static build in asset accounts, deploys with the
|
|
434
|
-
// burner wallet at ~/.openzoo/wallet.json, and `serve` runs the local
|
|
435
|
-
// gateway/explorer that maps http://localhost:4402/... onto the program.
|
|
436
|
-
const argv = process.argv.slice(2).map((a, i) => (i === 0 && a === 'transmute' ? 'help' : a));
|
|
437
|
-
// The transmuter ships inside this package (transmute/): lib/cli.js, the
|
|
438
|
-
// compiler, the gateway + hub and the on-chain runtime crate
|
|
439
|
-
// (transmute/runtime/zoo-host) all travel with npm — no second install.
|
|
440
|
-
await (await import('../transmute/lib/cli.js')).run(argv);
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
420
|
case 'address':
|
|
444
421
|
(await import('../lib/info.js')).printAddress();
|
|
445
422
|
break;
|
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/config.js
CHANGED
|
@@ -80,6 +80,40 @@ export const EVM_FUNDING_ASSETS = {
|
|
|
80
80
|
],
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* RPC endpoint BY CHAIN ID — the only lookup a 402 row can be resolved with.
|
|
85
|
+
*
|
|
86
|
+
* A row names its chain as `eip155:<id>` and nothing else. The rail NAMES
|
|
87
|
+
* above are a coarser bucket: railOf() collapses every chain that is not Base
|
|
88
|
+
* or Robinhood into the single name "evm", so a lookup keyed by rail name
|
|
89
|
+
* cannot tell Polygon from Arbitrum and answers with whatever the fallback
|
|
90
|
+
* branch happens to be.
|
|
91
|
+
*
|
|
92
|
+
* MEASURED 2026-09-09: the payment preflight did exactly that — for any
|
|
93
|
+
* non-Base EVM row it read the balance over the ROBINHOOD RPC, asked a Polygon
|
|
94
|
+
* USDC address that is not a contract there, threw, was swallowed by the
|
|
95
|
+
* preflight's catch, and signed an authorization for a chain the wallet held
|
|
96
|
+
* nothing on. The facilitator then reverted it and the caller was told
|
|
97
|
+
* "execution reverted" while 76 USDC sat on Base. Keyed by chain id, that
|
|
98
|
+
* cannot happen: either we can read the chain, or we say we cannot.
|
|
99
|
+
*/
|
|
100
|
+
export const EVM_RPC_BY_CHAIN = {
|
|
101
|
+
8453: () => config.baseRpcUrl,
|
|
102
|
+
4663: () => config.rhRpcUrl,
|
|
103
|
+
137: () => process.env.OPENZOO_POLYGON_RPC || 'https://polygon-rpc.com',
|
|
104
|
+
42161: () => process.env.OPENZOO_ARBITRUM_RPC || 'https://arb1.arbitrum.io/rpc',
|
|
105
|
+
10: () => process.env.OPENZOO_OPTIMISM_RPC || 'https://mainnet.optimism.io',
|
|
106
|
+
480: () => process.env.OPENZOO_WORLD_RPC || 'https://worldchain-mainnet.gateway.tenderly.co',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** RPC for a row's `eip155:<id>` network, or null when we cannot read that chain. */
|
|
110
|
+
export function evmRpcForNetwork(network) {
|
|
111
|
+
const m = /^eip155:(\d+)$/.exec(String(network || ''));
|
|
112
|
+
if (!m) return null;
|
|
113
|
+
const f = EVM_RPC_BY_CHAIN[Number(m[1])];
|
|
114
|
+
return f ? f() : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
/** RPC endpoint for an EVM rail name. */
|
|
84
118
|
export function evmRpcFor(rail) {
|
|
85
119
|
// A rail with no RPC here reports NO balance at all — silently. Every entry
|
package/lib/pay.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Connection } from '@solana/web3.js';
|
|
2
|
-
import { config, fundingLine, evmRpcFor } from './config.js';
|
|
2
|
+
import { config, fundingLine, evmRpcFor, evmRpcForNetwork } from './config.js';
|
|
3
3
|
import { loadOrCreateWallet } from './wallet.js';
|
|
4
4
|
import {
|
|
5
5
|
parse402, orderAccepts, railOf, buildPaymentOnline, tokenBalance,
|
|
@@ -123,6 +123,30 @@ const RAIL_MEMO_MS = Number(process.env.OPENZOO_RAIL_MEMO_MS || 15_000);
|
|
|
123
123
|
const underfundedUntil = new Map(); // asset -> epoch ms after which to re-try it
|
|
124
124
|
let lastGoodAsset = null;
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* One line to stderr about which rail was chosen or skipped. Quiet by default
|
|
128
|
+
* would defeat the point — this is the answer to "why did my deposit not get
|
|
129
|
+
* used", and it is only printed when a rail was actually passed over, so a
|
|
130
|
+
* normal single-rail call stays silent. OPENZOO_QUIET=1 turns it off.
|
|
131
|
+
*/
|
|
132
|
+
function railNote(msg) {
|
|
133
|
+
if (process.env.OPENZOO_QUIET === '1') return;
|
|
134
|
+
try { process.stderr.write(`openzoo: ${msg}\n`); } catch { /* stderr closed */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** "Base (eip155:8453)" — the name a human knows plus the id a log needs. */
|
|
138
|
+
function railLabel(accept) {
|
|
139
|
+
const n = chainName(accept?.network);
|
|
140
|
+
return n === accept?.network ? String(n) : `${n} (${accept.network})`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A chain a human recognises, for funding instructions. */
|
|
144
|
+
const CHAIN_NAMES = { 1: 'Ethereum', 10: 'Optimism', 137: 'Polygon', 480: 'World Chain', 4663: 'Robinhood Chain', 8453: 'Base', 42161: 'Arbitrum' };
|
|
145
|
+
export function chainName(network) {
|
|
146
|
+
const m = /^eip155:(\d+)$/.exec(String(network || ''));
|
|
147
|
+
return (m && CHAIN_NAMES[Number(m[1])]) || network;
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
const memoKey = (a) => `${a?.network || ''}|${a?.asset || ''}`;
|
|
127
151
|
|
|
128
152
|
/** lastGood first, known-underfunded last (never dropped — only deprioritised). */
|
|
@@ -197,8 +221,12 @@ export class PayClient {
|
|
|
197
221
|
const b = await tokenBalance(this.connection, this.keypair.publicKey, accept.asset);
|
|
198
222
|
return BigInt(b.raw || 0);
|
|
199
223
|
}
|
|
224
|
+
// By chain id, for the reason spelled out in buildPaymentFor: the rail name
|
|
225
|
+
// cannot tell one non-Base EVM chain from another.
|
|
226
|
+
const rpcUrl = evmRpcForNetwork(accept.network) ?? evmRpcFor(rail);
|
|
227
|
+
if (!rpcUrl) return 0n;
|
|
200
228
|
const raw = await evmTokenBalance({
|
|
201
|
-
rpcUrl
|
|
229
|
+
rpcUrl, token: accept.asset, owner: this.evmAddress,
|
|
202
230
|
});
|
|
203
231
|
return BigInt(raw || 0);
|
|
204
232
|
}
|
|
@@ -253,8 +281,26 @@ export class PayClient {
|
|
|
253
281
|
// Preflight: check the settlement-asset balance so an unfundable payment
|
|
254
282
|
// fails HERE with funding instructions instead of at the facilitator.
|
|
255
283
|
// Advisory only — an unreachable RPC never blocks a payment attempt.
|
|
256
|
-
|
|
284
|
+
//
|
|
285
|
+
// THE RPC IS CHOSEN BY THE ROW'S CHAIN ID, NEVER BY ITS RAIL NAME.
|
|
286
|
+
// railOf() calls every chain that is not Base or Robinhood "evm", so the
|
|
287
|
+
// old `rail === 'base' ? baseRpc : rhRpc` sent a POLYGON row's balance
|
|
288
|
+
// read to Robinhood Chain, where that token address is not a contract.
|
|
289
|
+
// The read threw, the catch below swallowed it, and this signed an
|
|
290
|
+
// authorization for a chain holding nothing — the facilitator reverted
|
|
291
|
+
// it and the caller was told "execution reverted" with a funded Base
|
|
292
|
+
// wallet one row away. MEASURED 2026-09-09.
|
|
293
|
+
const rpcUrl = evmRpcForNetwork(accept.network);
|
|
257
294
|
const owner = this.evmAddress;
|
|
295
|
+
// A CHAIN WE CANNOT READ IS NOT A CHAIN WE SIGN FOR. Skipping the row
|
|
296
|
+
// costs a caller nothing — the walk moves to the next one and reports
|
|
297
|
+
// honestly if none is payable — while signing it blind spends the call
|
|
298
|
+
// on a revert and teaches the caller nothing about why.
|
|
299
|
+
if (owner && !rpcUrl) {
|
|
300
|
+
throw new UnderfundedError(accept, null, owner, {
|
|
301
|
+
line: `No RPC configured for ${accept.network}, so this wallet's balance there cannot be checked — skipping that rail rather than signing blind. Set OPENZOO_POLYGON_RPC (or the matching chain's RPC) to use it.`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
258
304
|
if (owner) {
|
|
259
305
|
const bal = await evmTokenBalance({ rpcUrl, token: accept.asset, owner }).catch(() => null);
|
|
260
306
|
if (bal !== null && bal < BigInt(accept.maxAmountRequired)) {
|
|
@@ -263,7 +309,7 @@ export class PayClient {
|
|
|
263
309
|
// Paxos USDG, which implements EIP-3009 directly, so the only
|
|
264
310
|
// question left on every EVM rail is whether the wallet holds enough.
|
|
265
311
|
const sym = accept?.extra?.symbol || 'the quoted asset';
|
|
266
|
-
const where = rail === 'robinhood' ? 'Robinhood Chain' : rail === 'base' ? 'Base' : accept.network;
|
|
312
|
+
const where = rail === 'robinhood' ? 'Robinhood Chain' : rail === 'base' ? 'Base' : chainName(accept.network);
|
|
267
313
|
throw new UnderfundedError(accept, null, owner, {
|
|
268
314
|
line: await withOnrampLink(
|
|
269
315
|
`Send a few cents of ${sym} on ${where} to ${owner}.`,
|
|
@@ -340,6 +386,7 @@ export class PayClient {
|
|
|
340
386
|
}
|
|
341
387
|
try {
|
|
342
388
|
payment = await this.buildPaymentFor(cand, onStage, quote);
|
|
389
|
+
if (fundErrs.length) railNote(`paying on ${railLabel(cand)}`);
|
|
343
390
|
accept = cand;
|
|
344
391
|
lastGoodAsset = memoKey(cand);
|
|
345
392
|
underfundedUntil.delete(memoKey(cand));
|
|
@@ -352,6 +399,15 @@ export class PayClient {
|
|
|
352
399
|
// Remember it so the next call does not re-pay the discovery cost.
|
|
353
400
|
underfundedUntil.set(memoKey(cand), Date.now() + RAIL_MEMO_MS);
|
|
354
401
|
fundErrs.push({ sym: cand?.extra?.symbol || cand?.asset, err: e });
|
|
402
|
+
// SAY WHICH RAIL WAS PASSED OVER AND WHY.
|
|
403
|
+
//
|
|
404
|
+
// A caller whose wallet is funded on one chain and empty on five
|
|
405
|
+
// others sees only the LAST row's failure, and the chain that failed
|
|
406
|
+
// is not the chain they funded. Reading that as "my deposit is not
|
|
407
|
+
// seen" is the correct reading of what they were shown — the walk was
|
|
408
|
+
// silent about the five it skipped. One line per skip, on stderr,
|
|
409
|
+
// makes the choice inspectable without a flag or a debug build.
|
|
410
|
+
railNote(`skipping ${railLabel(cand)} — ${e.message.replace(/^openzoo wallet underfunded: /, '')}`);
|
|
355
411
|
}
|
|
356
412
|
}
|
|
357
413
|
if (!payment) {
|
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
|
|
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
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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);
|
package/lib/stripeOnramp.js
CHANGED
|
@@ -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}`;
|