openzoo 0.4.0 → 0.4.2

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
@@ -152,8 +152,10 @@ The receipt names which base you got; `extra.directUsd` / `extra.savesVsDirect`
152
152
  | rail | network | status |
153
153
  |---|---|---|
154
154
  | **Solana** (default) | `solana:5eykt…` | **live** — Token-2022 `TransferChecked`, partial-signed, gateway pays fees. Tested end-to-end against the production 402. Settlement uses a wrapped settlement mint as internal plumbing; you only ever hold and send USDC or TOKEN. |
155
- | Base | `eip155:8453` | implemented (standard x402 EIP-3009 `transferWithAuthorization`), **live-untested** the zoo's 402s currently offer only Solana rows. |
156
- | Robinhood Chain | `eip155:4663` | experimental, behind `OPENZOO_ENABLE_RH=1` the zoo ships this rail dark and facilitator settlement there is unverified. |
155
+ | Base | `eip155:8453` | **offered by the zoo** — standard x402 EIP-3009 `transferWithAuthorization` against native USDC. Fund the wallet's EVM address with USDC on Base; nothing is converted. Settlement from this package is live-untested. |
156
+ | Robinhood Chain | `eip155:4663` | experimental, behind `OPENZOO_ENABLE_RH=1`. The zoo quotes it, but its settlement asset has no conversion path here (conversion is Solana-only), so there is no plain balance you can fund and have the shim spend — use Solana or Base. |
157
+
158
+ `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.
157
159
 
158
160
  The rail is chosen from the 402's `accepts[]` itself (Solana first). 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 hardcodes `decimals = 6`; that's wrong for 18-decimal mints and this package deliberately does not copy the bug.)
159
161
 
package/lib/config.js CHANGED
@@ -28,9 +28,57 @@ export const FUNDING_ASSETS = [
28
28
  export const USDC_MINT = FUNDING_ASSETS[0].mint;
29
29
  export const TOKEN_MINT = FUNDING_ASSETS[1].mint;
30
30
 
31
- /** The one canonical way to tell a user how to fund. */
31
+ /**
32
+ * The one canonical way to tell a user how to fund the SOLANA rail. Callers
33
+ * pass the Solana address; the rail is named explicitly because the wallet
34
+ * also has an EVM address for the Base / Robinhood rails.
35
+ */
32
36
  export function fundingLine(address) {
33
- return `send a few cents of USDC (${USDC_MINT}) or TOKEN (${TOKEN_MINT}) to ${address}`;
37
+ return `send a few cents of USDC (${USDC_MINT}) or TOKEN (${TOKEN_MINT}) on Solana to ${address}`;
38
+ }
39
+
40
+ /**
41
+ * What a user funds each rail with — UNDERLYING assets only, and only assets
42
+ * this shim can actually spend from. The settlement mints the 402 quotes are
43
+ * internal plumbing and never appear in user-facing copy.
44
+ *
45
+ * Solana: quoted in settlement mints, converted from plain USDC / TOKEN at
46
+ * payment time (lib/wrap.js).
47
+ * Base: quoted in native USDC — funded and spent as-is, no conversion.
48
+ * Robinhood: quoted in a settlement asset with no conversion path on EVM
49
+ * (wrapping is Solana-only), so there is no plain balance a user can fund
50
+ * and have the shim pay from — hence no assets, and no funding line.
51
+ */
52
+ export const RAIL_FUNDING = {
53
+ solana: { label: 'Solana', assets: ['USDC', 'TOKEN'] },
54
+ base: { label: 'Base', assets: ['USDC'] },
55
+ robinhood: { label: 'Robinhood Chain', assets: [] },
56
+ };
57
+
58
+ /**
59
+ * "USDC or TOKEN on Solana · USDC on Base" — the funding hint for exactly the
60
+ * rails a live 402 is offering, derived from liveRails().live. Rails with no
61
+ * fundable underlying, and networks we have no funding copy for (an
62
+ * unrecognised chain the zoo starts quoting), are left out rather than guessed
63
+ * at. Returns '' when nothing is fundable.
64
+ */
65
+ export function railFundingHint(liveRailNames) {
66
+ return (liveRailNames || [])
67
+ .map((rail) => RAIL_FUNDING[rail])
68
+ .filter((spec) => spec?.assets.length)
69
+ .map((spec) => `${spec.assets.join(' or ')} on ${spec.label}`)
70
+ .join(' · ');
71
+ }
72
+
73
+ /**
74
+ * Rails the zoo is quoting right now that this shim cannot pay from a plain
75
+ * funded balance — named so a live rail never silently disappears from the
76
+ * funding advice.
77
+ */
78
+ export function unfundableRails(liveRailNames) {
79
+ return (liveRailNames || [])
80
+ .filter((rail) => RAIL_FUNDING[rail] && !RAIL_FUNDING[rail].assets.length)
81
+ .map((rail) => RAIL_FUNDING[rail].label);
34
82
  }
35
83
 
36
84
  /**
package/lib/info.js CHANGED
@@ -8,7 +8,7 @@ export function printAddress() {
8
8
  const { keypair, evmPrivateKey, created, path } = loadOrCreateWallet();
9
9
  if (created) console.log(`new burner wallet created at ${path} (chmod 600)`);
10
10
  console.log(keypair.publicKey.toBase58());
11
- console.log(`(evm, for Base/RH rails — untested: ${privateKeyToAccount(evmPrivateKey).address})`);
11
+ console.log(`(evm, for the Base / Robinhood rails: ${privateKeyToAccount(evmPrivateKey).address})`);
12
12
  }
13
13
 
14
14
  export async function printBalance() {
package/lib/mcp.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
5
- import { config, FUNDING_ASSETS } from './config.js';
5
+ import { config, FUNDING_ASSETS, liveRails, railFundingHint } from './config.js';
6
6
  import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
7
7
  import { tokenBalance } from './x402.js';
8
8
  import { askWithContext, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
@@ -94,10 +94,17 @@ export async function startMcp() {
94
94
  ]);
95
95
  const balances = { SOL: lamports / 1e9 };
96
96
  FUNDING_ASSETS.forEach((a, i) => { balances[a.symbol] = bals[i].ui ?? 0; });
97
+ // Funding advice follows the rails the zoo is quoting right now, not a
98
+ // hardcoded Solana assumption. Fail soft: the probe is advisory.
99
+ const rails = await liveRails().catch(() => null);
100
+ const hint = rails ? railFundingHint(rails.live) : '';
97
101
  return text({
98
102
  solanaAddress: client.address,
99
- fundWith: FUNDING_ASSETS.map((a) => `${a.symbol} (${a.mint})`).join(' or '),
100
- fundHint: 'send a few cents of either to this address — the shim wraps whichever the 402 quotes, at payment time',
103
+ evmAddress: client.evmAddress,
104
+ railsLiveNow: rails?.live ?? null,
105
+ fundWith: hint || `${FUNDING_ASSETS.map((a) => a.symbol).join(' or ')} on Solana`,
106
+ solanaMints: Object.fromEntries(FUNDING_ASSETS.map((a) => [a.symbol, a.mint])),
107
+ fundHint: 'send a few cents of a listed asset to the address for that rail — Solana assets to solanaAddress, Base assets to evmAddress. The shim converts to whatever the 402 quotes, at payment time.',
101
108
  balances,
102
109
  receipts: client.receipts.map((r) => ({ at: r.at, line: r.line })),
103
110
  });
package/lib/pay.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  receiptLine, decodeSettleHeader,
8
8
  } from './x402.js';
9
9
  import { buildEvmPayment } from './evm.js';
10
+ import { privateKeyToAccount } from 'viem/accounts';
10
11
  import {
11
12
  resolvePool, poolState, depositForShares, buildWrapInstructions, sendWrap,
12
13
  } from './wrap.js';
@@ -50,6 +51,12 @@ export class PayClient {
50
51
 
51
52
  get address() { return this.keypair.publicKey.toBase58(); }
52
53
 
54
+ /** The EVM address the Base / Robinhood rails pay from — same wallet file. */
55
+ get evmAddress() {
56
+ if (!this.evmPrivateKey) return null;
57
+ try { return privateKeyToAccount(this.evmPrivateKey).address; } catch { return null; }
58
+ }
59
+
53
60
  async buildPaymentFor(accept, onStage) {
54
61
  const rail = railOf(accept);
55
62
  if (rail === 'solana') {
package/lib/proxy.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import http from 'node:http';
2
2
  import { Readable } from 'node:stream';
3
- import { config, FUNDING_ASSETS, fundingLine, liveRails } from './config.js';
3
+ import {
4
+ config, FUNDING_ASSETS, fundingLine, liveRails, railFundingHint, unfundableRails, RAIL_FUNDING,
5
+ } from './config.js';
4
6
  import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
5
7
  import { tokenBalance } from './x402.js';
6
8
  import { bindCorpus, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
@@ -177,7 +179,8 @@ export async function startProxy({ silent = false } = {}) {
177
179
  console.log(`listening on http://localhost:${config.port}/v1`);
178
180
  console.log('');
179
181
  if (client.walletCreated) console.log(`new burner wallet created at ${client.walletPath} (chmod 600)`);
180
- console.log(`wallet (fund me): ${client.address}`);
182
+ console.log(`wallet (fund me) · solana: ${client.address}`);
183
+ if (client.evmAddress) console.log(`wallet (fund me) · evm (base / robinhood): ${client.evmAddress}`);
181
184
  try {
182
185
  const bals = await Promise.all(
183
186
  FUNDING_ASSETS.map((a) => tokenBalance(client.connection, client.keypair.publicKey, a.mint)),
@@ -193,7 +196,17 @@ export async function startProxy({ silent = false } = {}) {
193
196
  try {
194
197
  const rails = await liveRails();
195
198
  if (rails) {
196
- console.log(`rails live now: ${rails.live.join(' · ')} (fund with USDC or TOKEN on solana)`);
199
+ console.log(`rails live now: ${rails.live.join(' · ')}`);
200
+ // Funding advice is derived from those rails, never hardcoded — the
201
+ // zoo can add a chain without this package shipping again.
202
+ const hint = railFundingHint(rails.live);
203
+ if (hint) console.log(`fund with: ${hint}`);
204
+ const unfundable = unfundableRails(rails.live);
205
+ if (unfundable.length) {
206
+ const fundable = rails.live.filter((r) => RAIL_FUNDING[r]?.assets.length).map((r) => RAIL_FUNDING[r].label);
207
+ const instead = fundable.length ? `pay from ${fundable.join(' or ')} instead` : 'no fundable rail is offered right now';
208
+ console.log(`note: ${unfundable.join(' / ')} is offered by the zoo but not fundable from a plain balance here — ${instead}.`);
209
+ }
197
210
  if (rails.dark.length) {
198
211
  const rh = rails.dark.includes('robinhood') ? ' — robinhood also needs OPENZOO_ENABLE_RH=1' : '';
199
212
  console.log(`rails implemented but not offered by the zoo right now: ${rails.dark.join(' · ')}${rh}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.4.0",
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 rail live; Base/Robinhood rails experimental.",
3
+ "version": "0.4.2",
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.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {