openzoo 0.49.15 → 0.49.17

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
@@ -218,11 +218,11 @@ Pin the key with `OPENZOO_TUNNEL_TOKEN` if your IDE stores it. Keys never leave
218
218
  - **USDC** — `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`
219
219
  - **TOKEN** — `EVULoNF4DeMBN4dGiZiDfpiiTfNZgoCvXWWgaV3epump`
220
220
 
221
- Both are first-class: each is the underlying of a live Solana rail the zoo quotes (USDC yUSDCx, TOKEN → wTOKENx). That's the whole funding story the shim converts whichever the 402 quotes internally, at payment time, for exactly the amount needed.
222
- - **SOL is optional but nice.** The gateway sponsors payment-transaction fees. If the wallet holds a pinch of SOL (~0.003), the internal conversion settles as its own transaction first; with zero SOL the conversion rides inside the gateway-sponsored payment transaction instead.
223
- - **The same wallet file also holds an EVM key.** `npx openzoo address` prints both addresses: the Solana one, and the EVM one used by the Base and Robinhood Chain rails. Fund Base with plain **USDC on Base** (nothing is converted); for Robinhood Chain, hold the plain token you want to pay with (USDG or a quoted memecoin) plus a sliver of RH ETH for gas conversion happens automatically at payment time.
221
+ Both are first-class, and both are the **exact mints the 402 quotes**. That is the whole funding story: fund one, spend it. Nothing is wrapped, converted, or deposited into a vault on the way the shim signs one transfer of the mint the row names.
222
+ - **SOL is optional.** The gateway sponsors payment-transaction fees, so a wallet already holding the quoted mint needs no SOL. Keep a pinch (~0.003) only for the rent if the token account does not exist yet.
223
+ - **The same wallet file also holds an EVM key.** `npx openzoo address` prints both addresses: the Solana one, and the EVM one used by the Base and Robinhood Chain rails. Fund Base with **USDC on Base** and Robinhood Chain with **USDG** canonical Paxos USDG, which implements EIP-3009 directly. Same rule as Solana: nothing is converted.
224
224
  - **Spend caps.** The proxy refuses any single quote above `OPENZOO_MAX_USD_PER_CALL` (default $0.50).
225
- - `npx openzoo balance` — funds on every rail, grouped per chain: Solana (USDC + TOKEN + SOL), Base (USDC + ETH), Robinhood Chain (USDG, the ODDBALLER / IOU / ROBINHOODS memecoins, ETH). USD value where a price is known; `$?` where not. `npx openzoo address` — both funding addresses.
225
+ - `npx openzoo balance` — funds on every rail, grouped per chain: Solana (USDC + TOKEN + LEOS + SOL), Base (USDC + ETH), Robinhood Chain (USDG + ETH). USD value where a price is known; `$?` where not. `npx openzoo address` — both funding addresses.
226
226
 
227
227
  ## Honest pricing note
228
228
 
@@ -231,7 +231,7 @@ Two bases, reported per call in the 402 (`extra.pricing`):
231
231
  - **Big bodies price at a counterfactual discount** (~10× cheaper than buying the same call direct) — the zoo's leCore memory means it never forwards your whole body upstream, and passes most of the savings on. Measured numbers at [benches.openzoo.fun](https://benches.openzoo.fun).
232
232
  - **Asks against a bound corpus price on the small forwarded body.** That is not a discount trick: a few hundred tokens at OpenRouter rates is far below the counterfactual price of shipping the corpus, which is the whole point of binding once.
233
233
 
234
- The receipt names which base you got; `extra.billedUsd` / `extra.directUsd` / `extra.savedUsd` let you check the math. (Card subscription pricing is a different path.)
234
+ The receipt names which base you got; `extra.billedUsd` / `extra.directUsd` / `extra.savedUsd` let you check the math. Pay-per-request is the only lane — there is no subscription or API key.
235
235
 
236
236
  ## Payment rails
237
237
 
@@ -239,11 +239,11 @@ All three rails have settled real payments (2026-08-14):
239
239
 
240
240
  | rail | network | status |
241
241
  |---|---|---|
242
- | **Solana** (default) | `solana:5eykt…` | **live** — Token-2022 payment `TransferChecked`, partial-signed, gateway pays fees. Settles daily; tested end-to-end against the production 402. Settlement uses a wrapped mint as internal plumbing; you only ever hold and send USDC or TOKEN. Funding wrap is a 9-account ix (program pulls the deposit; old 5-account wrap is rejected `0x6a`). |
242
+ | **Solana** (default) | `solana:5eykt…` | **live** — `TransferChecked` of the raw native mint, partial-signed, gateway pays fees. The message is a fixed four-instruction layout the facilitator checks by position: compute-unit limit, compute-unit price, `TransferChecked` third, memo fourth. You hold and send exactly what the 402 quotes. |
243
243
  | Base | `eip155:8453` | **live** — standard x402 EIP-3009 `transferWithAuthorization` against native USDC, batched settle through the facilitator. Fund the wallet's EVM address with USDC on Base; nothing is converted. |
244
- | Robinhood Chain | `eip155:4663` | **live** — EIP-3009, batched settle through the facilitator. Hold the plain token a row is quoted in (USDG, or the ODDBALLER / IOU / ROBINHOODS memecoins) and the shim converts exactly enough at payment time, automatically two small on-chain steps paid from the wallet's own RH ETH. No gas? The error says exactly how much ETH to send and where. Default rail selection skips this chain unless `OPENZOO_ENABLE_RH=1`; `OPENZOO_RAIL=robinhood` forces it outright. |
244
+ | Robinhood Chain | `eip155:4663` | **live** — EIP-3009, batched settle through the facilitator, against canonical Paxos USDG. Fund the wallet's EVM address with USDG; nothing is converted, and no gas token is needed to pay. Default rail selection skips this chain unless `OPENZOO_ENABLE_RH=1`; `OPENZOO_RAIL=robinhood` forces it outright. |
245
245
 
246
- `npx openzoo` prints the rails off a live 402 at startup, and the funding line is derived from those rails — so a new chain (the wrapped RH memecoin twins are in-deploy, for example) shows up without this package shipping again.
246
+ `npx openzoo` prints the rails off a live 402 at startup, and the funding line is derived from those rails — so a new chain shows up without this package shipping again.
247
247
 
248
248
  The rail is chosen from the 402's `accepts[]` itself (Solana first). **Steer it with `OPENZOO_RAIL=solana|base|robinhood`** — the picker then uses only that rail, and errors clearly if the live 402 does not offer it. Amounts are always taken as raw units from the 402, and Solana decimals are read from the mint **on-chain** — never hardcoded. (The zoo's own pasted prompt used to hardcode `decimals = 6`; this package deliberately does not copy the bug.)
249
249
 
package/bin/openzoo.js CHANGED
@@ -103,8 +103,6 @@ usage:
103
103
  npx openzoo vscode [path] same, for VS Code
104
104
  npx openzoo editor [path] whichever is installed (Cursor wins if both)
105
105
  npx openzoo claude [dir] Claude Code CLI on the zoo (x402 per turn); --desktop for the app
106
- npx openzoo xbot @openzoobot on X: 1 free question per account, then x402
107
- --once (single poll) --dry-run (answer, do not post)
108
106
  by default, --terminal for the Claude Code CLI
109
107
  npx openzoo launch <cmd> [args] launch a TERMINAL Messages API client
110
108
  (claude, aider...) already pointed at the zoo
@@ -185,15 +183,6 @@ async function main() {
185
183
  case 'mcp':
186
184
  await (await import('../lib/mcp.js')).startMcp();
187
185
  break;
188
- case 'xbot': {
189
- const rest = process.argv.slice(3);
190
- await (await import('../lib/xbot.js')).runXBot({
191
- once: rest.includes('--once'),
192
- dryRun: rest.includes('--dry-run'),
193
- seed: rest.includes('--seed'),
194
- });
195
- break;
196
- }
197
186
  case 'claude':
198
187
  await (await import('../lib/launch.js')).launchClaude(process.argv.slice(3));
199
188
  break;
package/lib/config.js CHANGED
@@ -9,13 +9,13 @@ export const config = {
9
9
  // rate-limits hard under any real load (429s during demos). OPENZOO_RPC
10
10
  // overrides for anyone who wants their own.
11
11
  rpcUrl: process.env.OPENZOO_RPC || 'https://eu.fluxrpc.com?key=ab9278e1-6430-41ab-aee0-ac6b759a1fe4',
12
- // Which accepts[] row to pay with, by extra.symbol (internal rail selector).
13
- // wTOKENx3 (the HSyC twin of TOKEN, fixed wrap program) is the DEFAULT
14
- // preference: a wallet holding TOKEN wraps-and-pays in TOKEN. The picker
15
- // walks best-first and takes the first AFFORDABLE row, so wallets without
16
- // TOKEN fall through to yUSDCx/USDC exactly as before. OPENZOO_TOKEN
17
- // overrides.
18
- token: process.env.OPENZOO_TOKEN || 'wTOKENx3',
12
+ // Which accepts[] row to prefer, by extra.symbol (internal rail selector).
13
+ // TOKEN is the default so a wallet holding it spends it. The picker walks
14
+ // best-first and takes the first AFFORDABLE row, so wallets without TOKEN
15
+ // fall through to USDC. This named the wTOKENx3 twin until the gateway went
16
+ // native a symbol no row carries any more, which silently disabled the
17
+ // preference rather than erroring. OPENZOO_TOKEN overrides.
18
+ token: process.env.OPENZOO_TOKEN || 'TOKEN',
19
19
  // Force a rail: 'solana' | 'base' | 'robinhood'. Unset -> Solana-first order.
20
20
  // pickAccept errors clearly when the live 402 does not offer the forced rail.
21
21
  rail: (process.env.OPENZOO_RAIL || '').toLowerCase() || null,
@@ -32,18 +32,16 @@ export const config = {
32
32
  };
33
33
 
34
34
  /**
35
- * Assets a user can fund the burner wallet with. Both are first-class: each is
36
- * the underlying of a live Solana rail the zoo quotes (USDC -> yUSDCx,
37
- * TOKEN -> wTOKENx), and the shim wraps whichever one it needs at payment time,
38
- * for exactly the amount needed. Never mention one of these without the other.
35
+ * Assets a user can fund the burner wallet with. All three are first-class and
36
+ * these are the EXACT mints the 402 quotes fund one, spend it, no conversion
37
+ * step. Never mention one of these without the others.
39
38
  */
40
39
  export const FUNDING_ASSETS = [
41
40
  { symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' },
42
41
  { symbol: 'TOKEN', mint: 'EVULoNF4DeMBN4dGiZiDfpiiTfNZgoCvXWWgaV3epump' },
43
- // LEOS settles through the wLEOSx twin, on the same terms as TOKEN. NINE
44
- // decimals, and a LEGACY SPL mint rather than Token-2022 balance reads must
45
- // not assume either, so nothing here hardcodes them; decimals come off the
46
- // mint on chain.
42
+ // LEOS is NINE decimals, and a LEGACY SPL mint rather than Token-2022
43
+ // balance reads must not assume either, so nothing here hardcodes them;
44
+ // decimals come off the mint on chain.
47
45
  { symbol: 'LEOS', mint: '5xgsnby6P9zqGK71J7H4yJLxzqPvNbC7rDZxNzjHmj7e' },
48
46
  ];
49
47
 
@@ -54,27 +52,45 @@ export const LEOS_MINT = FUNDING_ASSETS[2].mint;
54
52
  /**
55
53
  * EVM-side balances `openzoo balance` reports, grouped by rail. `usd` is a
56
54
  * known price (stables); null means "no honest price here" and the CLI prints
57
- * `?` rather than inventing one. The memecoins are Robinhood Chain ERC-20s
58
- * (18 decimals, verified on-chain 2026-08-14); their wrapped x402 twins are
59
- * in-deploy and will appear in the 402 itself when live.
55
+ * `?` rather than inventing one.
56
+ *
57
+ * ONLY LIST WHAT THE 402 ACCEPTS. The Robinhood memecoins (ODDBALLER / IOU /
58
+ * ROBINHOODS) were removed 2026-08-24 along with every wrapped rail: they were
59
+ * only ever payable through X402Wrapper twins the payer had to mint first, and
60
+ * the gateway no longer offers them. Reporting a balance the user cannot spend
61
+ * reads as "you have funds" and then fails at payment.
60
62
  */
61
63
  export const EVM_FUNDING_ASSETS = {
62
64
  base: [
63
65
  { symbol: 'USDC', address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6, usd: 1 },
64
66
  ],
65
67
  robinhood: [
68
+ // Canonical Paxos USDG, pinned by ADDRESS: six impersonators on this chain
69
+ // answer symbol() == "USDG" with 41k-111k holders each.
66
70
  { symbol: 'USDG', address: '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168', decimals: 6, usd: 1 },
67
- { symbol: 'ODDBALLER', address: '0x923eb7BD5B84a1a114CB57212cE2F2e87AE60E2A', decimals: 18, usd: null },
68
- // note rides into the banner CA block: IOU is accepted by design, but its
69
- // 402 row fails closed whenever DexScreener has no pool to price it from.
70
- { symbol: 'IOU', address: '0xf391999FACbEE613D4024191Dd31060540BF0bEd', decimals: 18, usd: null, note: 'accepted — quoting paused while its price feed is down' },
71
- { symbol: 'ROBINHOODS', address: '0xC42cF61C16aaC797b991cf9C1ac8Ae70bA74A286', decimals: 18, usd: null },
71
+ ],
72
+ arbitrum: [
73
+ { symbol: 'USDC', address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', decimals: 6, usd: 1 },
74
+ ],
75
+ optimism: [
76
+ { symbol: 'USDC', address: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', decimals: 6, usd: 1 },
77
+ ],
78
+ world: [
79
+ { symbol: 'USDC', address: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1', decimals: 6, usd: 1 },
72
80
  ],
73
81
  };
74
82
 
75
83
  /** RPC endpoint for an EVM rail name. */
76
84
  export function evmRpcFor(rail) {
77
- return rail === 'base' ? config.baseRpcUrl : rail === 'robinhood' ? config.rhRpcUrl : null;
85
+ // A rail with no RPC here reports NO balance at all — silently. Every entry
86
+ // in EVM_FUNDING_ASSETS needs a row, or `openzoo balance` just omits the
87
+ // chain and the user concludes they hold nothing there.
88
+ if (rail === 'base') return config.baseRpcUrl;
89
+ if (rail === 'robinhood') return config.rhRpcUrl;
90
+ if (rail === 'arbitrum') return process.env.OPENZOO_ARBITRUM_RPC || 'https://arb1.arbitrum.io/rpc';
91
+ if (rail === 'optimism') return process.env.OPENZOO_OPTIMISM_RPC || 'https://mainnet.optimism.io';
92
+ if (rail === 'world') return process.env.OPENZOO_WORLD_RPC || 'https://worldchain-mainnet.gateway.tenderly.co';
93
+ return null;
78
94
  }
79
95
 
80
96
  /**
@@ -87,27 +103,19 @@ export function fundingLine(address) {
87
103
  }
88
104
 
89
105
  /**
90
- * What a user funds each rail with UNDERLYING assets only, and only assets
91
- * this shim can actually spend from. The settlement mints the 402 quotes are
92
- * internal plumbing and never appear in user-facing copy.
93
- *
94
- * Solana: quoted in settlement mints, converted from plain USDC / TOKEN at
95
- * payment time (lib/wrap.js).
96
- * Base: quoted in native USDC — funded and spent as-is, no conversion.
97
- * Robinhood: the rail settles (real payments 2026-08-14) and the shim
98
- * auto-converts at payment time (lib/evmwrap.js: approve + deposit into the
99
- * quoted vault, discovered on-chain via asset() — never hardcoded). Fund
100
- * with the PLAIN tokens; the two conversion txs are the wallet's own, so a
101
- * sliver of RH ETH for gas is also needed — the `note` rides the hint.
106
+ * What a user funds each rail with. Every rail is quoted in the mint the user
107
+ * funds fund it, spend it, nothing in between. This list used to distinguish
108
+ * "underlying" from the settlement mint because the 402 named Token-2022 and
109
+ * ERC-4626 twins the shim minted at payment time; that distinction is gone
110
+ * along with the twins, and the two are now the same asset on every rail.
102
111
  */
103
112
  export const RAIL_FUNDING = {
104
113
  solana: { label: 'Solana', assets: ['USDC', 'TOKEN', 'LEOS'] },
105
114
  base: { label: 'Base', assets: ['USDC'] },
106
- robinhood: {
107
- label: 'Robinhood Chain',
108
- assets: ['USDG', 'ODDBALLER', 'IOU', 'ROBINHOODS'],
109
- note: 'plus a sliver of RH ETH for the conversion gas',
110
- },
115
+ robinhood: { label: 'Robinhood Chain', assets: ['USDG'] },
116
+ arbitrum: { label: 'Arbitrum', assets: ['USDC'] },
117
+ optimism: { label: 'Optimism', assets: ['USDC'] },
118
+ world: { label: 'World Chain', assets: ['USDC'] },
111
119
  };
112
120
 
113
121
  /**
@@ -163,7 +171,6 @@ export function unfundableRails(liveRailNames) {
163
171
  * What the resource will actually settle, right now, read off a live 402 —
164
172
  * never a hardcoded claim. Rails the code implements but the zoo is not
165
173
  * currently offering are reported separately so nobody funds a dead lane.
166
- * Underlying assets only: the wrapped settlement mints are internal plumbing.
167
174
  */
168
175
  export async function liveRails() {
169
176
  const r = await fetchHeaders(`${config.apiBase}/v1/chat/completions`, {
package/lib/demo.js CHANGED
@@ -2,7 +2,6 @@ import { config, fundingLine } from './config.js';
2
2
  import { PayClient, UnderfundedError } from './pay.js';
3
3
  import { parse402, pickAccept, tokenBalance } from './x402.js';
4
4
  import { startSpinner, postWithUploadSignal } from './spinner.js';
5
- import { resolvePool, poolState, depositForShares } from './wrap.js';
6
5
  import { bindCorpus, askWithContext } from './hrr.js';
7
6
  import { forgetContext } from './contexts.js';
8
7
 
@@ -49,17 +48,15 @@ async function quoteFor(bodyObj, headers = {}) {
49
48
  }
50
49
  }
51
50
 
52
- /** Can the wallet cover this quote (directly or by converting its USDC)? */
51
+ /**
52
+ * Can the wallet cover this quote? The 402 names the raw mint the wallet
53
+ * already holds, so this is one balance read — it used to also price the
54
+ * wallet's plain token against a wrap pool, which no longer exists.
55
+ */
53
56
  async function coverage(connection, owner, accept) {
54
57
  const need = BigInt(accept.maxAmountRequired);
55
58
  const have = await tokenBalance(connection, owner, accept.asset);
56
- if (have.raw >= need) return { covered: true, usdcUi: null };
57
- const pool = await resolvePool(connection, accept.asset).catch(() => null);
58
- if (!pool) return { covered: false, usdcUi: 0 };
59
- const { reserves, supply } = await poolState(connection, pool);
60
- const deposit = depositForShares(need - have.raw, reserves, supply);
61
- const underlying = await tokenBalance(connection, owner, pool.underlying.toBase58());
62
- return { covered: underlying.raw >= deposit, usdcUi: underlying.ui ?? 0 };
59
+ return { covered: have.raw >= need, usdcUi: have.ui ?? 0 };
63
60
  }
64
61
 
65
62
  export async function runDemo() {
@@ -135,7 +132,7 @@ export async function runDemo() {
135
132
  if (!cov.covered) {
136
133
  console.log('3) wallet not funded yet, so stopping before payment.');
137
134
  console.log(` quote : ≈ $${Number(x.billedUsd).toFixed(6)}`);
138
- console.log(` wallet: $${(cov.usdcUi ?? 0).toFixed(2)} USDC`);
135
+ console.log(` wallet: ${cov.usdcUi ?? 0} ${accept.extra?.symbol || 'of the quoted asset'}`);
139
136
  console.log(` fund : ${fundingLine(pub)}`);
140
137
  console.log(' then re-run `npx openzoo demo`.');
141
138
  return;
package/lib/evm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { createPublicClient, http } from 'viem';
3
3
  import { privateKeyToAccount } from 'viem/accounts';
4
- import { evmChainId } from './x402.js';
4
+ import { evmChainId, paymentEnvelope, encodeEnvelope } from './x402.js';
5
5
 
6
6
  const ERC20_ABI = [
7
7
  { type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'a', type: 'address' }], outputs: [{ type: 'uint256' }] },
@@ -26,10 +26,17 @@ export async function evmNativeBalance({ rpcUrl, owner }) {
26
26
  /**
27
27
  * EVM rails (Base eip155:8453; Robinhood Chain eip155:4663 behind OPENZOO_ENABLE_RH).
28
28
  *
29
- * Standard x402 v1 EVM "exact" scheme: an EIP-3009 transferWithAuthorization
30
- * typed-data signature; the facilitator submits it. X-PAYMENT = base64 of
31
- * { x402Version: 1, scheme: "exact", network: "<as offered>",
32
- * payload: { signature, authorization: { from, to, value, validAfter, validBefore, nonce } } }
29
+ * x402 EVM "exact" scheme: an EIP-3009 transferWithAuthorization typed-data
30
+ * signature; the facilitator submits it. The envelope (paymentEnvelope in
31
+ * x402.js) carries `resource` and the verbatim `accepted` row alongside the
32
+ * payload a CDP facilitator validates the payload against a schema and
33
+ * answers "'paymentPayload' is invalid: must match one of [x402V2Pay…]"
34
+ * without either, before it looks at the money. MEASURED 2026-08-25.
35
+ *
36
+ * The typed data is still signed against the chain id parsed from the ROW's
37
+ * network: `verifyNetworkFor` may rewrite the envelope's network to whatever
38
+ * the verifier enforces ("base"), but the EIP-712 domain has to name the chain
39
+ * the token actually lives on or the signature recovers to nobody.
33
40
  *
34
41
  * IMPORTANT — the decimals landmine does not exist here BY CONSTRUCTION:
35
42
  * authorization.value is the raw uint256 straight from maxAmountRequired.
@@ -41,7 +48,7 @@ export async function evmNativeBalance({ rpcUrl, owner }) {
41
48
  * Chain (eip155:4663), both via EIP-3009 batched settlement through the
42
49
  * facilitator. Steer to a rail with OPENZOO_RAIL.
43
50
  */
44
- export async function buildEvmPayment({ accept, evmPrivateKey }) {
51
+ export async function buildEvmPayment({ accept, evmPrivateKey, challenge }) {
45
52
  const chainId = evmChainId(accept.network);
46
53
  if (!chainId) throw new Error(`cannot parse EVM chain id from network ${accept.network}`);
47
54
  const account = privateKeyToAccount(evmPrivateKey);
@@ -77,22 +84,18 @@ export async function buildEvmPayment({ accept, evmPrivateKey }) {
77
84
  message: authorization,
78
85
  });
79
86
 
80
- const envelope = {
81
- x402Version: 1,
82
- scheme: accept.scheme,
83
- network: accept.network,
84
- payload: {
85
- signature,
86
- authorization: {
87
- ...authorization,
88
- value: authorization.value.toString(),
89
- validAfter: authorization.validAfter.toString(),
90
- validBefore: authorization.validBefore.toString(),
91
- },
87
+ const payload = {
88
+ signature,
89
+ authorization: {
90
+ ...authorization,
91
+ value: authorization.value.toString(),
92
+ validAfter: authorization.validAfter.toString(),
93
+ validBefore: authorization.validBefore.toString(),
92
94
  },
93
95
  };
94
96
  return {
95
- header: Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64'),
97
+ header: encodeEnvelope(paymentEnvelope(challenge, accept, payload)),
98
+ payload,
96
99
  ownerSignature: signature,
97
100
  from: account.address,
98
101
  };