openzoo 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 staccDOTsol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # openzoo
2
+
3
+ Local x402-paying proxy + MCP server for [openzoo.fun](https://openzoo.fun) — point **any** OpenAI-compatible harness (Cursor, Claude Code, aider, raw SDKs) at `localhost` and it pays the zoo per call from a local burner wallet. No account, no API key, no crypto knowledge required by your tools.
4
+
5
+ ```
6
+ npx openzoo
7
+ ```
8
+
9
+ That's it. First run generates a burner wallet at `~/.openzoo/wallet.json` (chmod 600), prints the funding address, and starts the proxy:
10
+
11
+ ```
12
+ base_url = http://localhost:8402/v1
13
+ api_key = sk-openzoo # any value works; the zoo takes payment, not keys
14
+ ```
15
+
16
+ Every request forwards to the zoo. When the zoo answers `402 Payment Required`, the proxy reads the quote, signs a token transfer from your local wallet, retries with the `X-PAYMENT` header, and streams the response back — SSE included, unbuffered. Each payment prints a one-line receipt:
17
+
18
+ ```
19
+ paid $0.002137 (9.5× cheaper than direct) · rail solana · tx 5Kd…
20
+ ```
21
+
22
+ ## 60-second quickstarts
23
+
24
+ **Cursor** (Settings → Models → OpenAI API): set *Override OpenAI Base URL* to `http://localhost:8402/v1`, API key `sk-openzoo`. (Cursor Hobby can't BYOK; Pro can.)
25
+
26
+ **Claude Code / any OpenAI-env tool:**
27
+ ```bash
28
+ export OPENAI_BASE_URL=http://localhost:8402/v1
29
+ export OPENAI_API_KEY=sk-openzoo
30
+ ```
31
+
32
+ **aider:**
33
+ ```bash
34
+ aider --openai-api-base http://localhost:8402/v1 --openai-api-key sk-openzoo --model openai/nvidia/nemotron-3.5-lightning
35
+ ```
36
+
37
+ **curl:**
38
+ ```bash
39
+ curl http://localhost:8402/v1/chat/completions -H 'Content-Type: application/json' \
40
+ -d '{"model":"nvidia/nemotron-3.5-lightning","messages":[{"role":"user","content":"hi"}]}'
41
+ ```
42
+
43
+ Model ids come from `GET http://localhost:8402/v1/models` (free, no payment).
44
+
45
+ ## Use as MCP
46
+
47
+ The same package is an MCP server sharing the same wallet and payment core:
48
+
49
+ ```bash
50
+ claude mcp add openzoo -- npx -y openzoo mcp
51
+ ```
52
+
53
+ Other MCP hosts:
54
+
55
+ ```json
56
+ { "mcpServers": { "openzoo": { "command": "npx", "args": ["-y", "openzoo", "mcp"] } } }
57
+ ```
58
+
59
+ Tools:
60
+ - **`zoo_ask`** — `{prompt, corpus?, model?, max_tokens?}`. The flagship: hand it a *huge* `corpus` (hundreds of thousands to ~1M tokens — a body the model itself would refuse) and a question; the zoo's leCore memory spills the corpus so the model reads only a few thousand tokens. Returns the answer plus the receipt (`billedUsd`, `savesVsDirect`, tokens actually read). An MCP-capable agent can delegate a giant-context question without touching its own model config.
61
+ - **`zoo_models`** — list the zoo's models and pricing (free).
62
+ - **`zoo_wallet`** — funding address, balances, and this session's receipts.
63
+
64
+ ## The demo
65
+
66
+ ```bash
67
+ npx openzoo demo
68
+ ```
69
+
70
+ Builds a ~965k-token document with one planted fact, shows that buying direct refuses it (*"The input token count exceeds the maximum number of tokens allowed."* — recorded, see [benches.openzoo.fun](https://benches.openzoo.fun)), then fetches the live x402 quote for the same body — the quote is free:
71
+
72
+ ```
73
+ quote: $0.009747 yUSDCx · pricing=counterfactual · direct would be $0.097474 · savesVsDirect=10.0×
74
+ ```
75
+
76
+ If the wallet is funded it pays (capped at `OPENZOO_DEMO_MAX_USD`, default $0.01) and prints the answer, the tokens the model actually read, and the receipt. If not, it prints exactly what to fund.
77
+
78
+ ## The wallet model
79
+
80
+ - **Burner, local, yours.** A keypair in `~/.openzoo/wallet.json`, created on first run, chmod 600. Keys never leave your machine — the zoo only ever sees signed transfers.
81
+ - **Fund it with yUSDCx** (mint `6ZjjxcoicqM4nniddkuPVwew4PDwY3swbfHsGbCuLuTv`), the $1-stable rail: wrap USDC at [x402.accrue.fund/start](https://x402.accrue.fund/start) and send to the address `npx openzoo address` prints. A few cents goes a long way.
82
+ - **You don't need SOL.** The gateway is the transaction feePayer; you only sign the token transfer.
83
+ - **Spend caps.** The proxy refuses any single quote above `OPENZOO_MAX_USD_PER_CALL` (default $0.50).
84
+ - `npx openzoo balance` / `npx openzoo address` — check funds / print the address.
85
+
86
+ ## Honest pricing note
87
+
88
+ Two bases, reported per call in the 402 (`extra.pricing`):
89
+ - **Short prompts price at a markup** (3× provider cost) — there's nothing to spill, you're paying for passthrough.
90
+ - **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 the savings on. Measured numbers at [benches.openzoo.fun](https://benches.openzoo.fun).
91
+
92
+ The receipt names which base you got; `extra.directUsd` / `extra.savesVsDirect` let you check the math.
93
+
94
+ ## Payment rails
95
+
96
+ | rail | network | status |
97
+ |---|---|---|
98
+ | **Solana** (default) | `solana:5eykt…` | **live** — Token-2022 `TransferChecked`, partial-signed, gateway pays fees. Tested end-to-end against the production 402. |
99
+ | Base | `eip155:8453` | implemented (standard x402 EIP-3009 `transferWithAuthorization`), **live-untested** — the zoo's 402s currently offer only Solana rows. |
100
+ | Robinhood Chain | `eip155:4663` | experimental, behind `OPENZOO_ENABLE_RH=1` — the zoo ships this rail dark and facilitator settlement there is unverified. |
101
+
102
+ 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.)
103
+
104
+ ## Configuration
105
+
106
+ | env | default | |
107
+ |---|---|---|
108
+ | `OPENZOO_PORT` | `8402` | proxy port |
109
+ | `OPENZOO_API_BASE` | `https://x402-tokens.fly.dev` | the zoo's door |
110
+ | `OPENZOO_RPC` | mainnet-beta public RPC | Solana RPC |
111
+ | `OPENZOO_TOKEN` | `yUSDCx` | which 402 row to pay |
112
+ | `OPENZOO_WALLET` | `~/.openzoo/wallet.json` | wallet path |
113
+ | `OPENZOO_MAX_USD_PER_CALL` | `0.5` | refuse quotes above this |
114
+ | `OPENZOO_DEMO_MAX_USD` | `0.01` | demo spend cap |
115
+ | `OPENZOO_ENABLE_RH` | `0` | allow the Robinhood rail |
116
+
117
+ ## What's tested
118
+
119
+ - Unit tests run against a **captured production 402 body** (`test/fixtures/live-402.json`): parsing, rail selection, and byte-level verification of the signed Solana transaction and the EVM typed-data payload.
120
+ - Live-verified: `/v1/models` through the proxy (200), 402 parse + quote + wallet check against mainnet, on-chain mint/decimals read, and full payment construction (signed, not broadcast) against a production quote.
121
+ - **Not yet live-verified: an actual settled paid call** — this session's wallet was unfunded, so settlement (the facilitator accepting the signed transfer) is untested from this client. The construction matches the gateway's own documented flow byte-for-byte.
122
+
123
+ MIT
package/bin/openzoo.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ const cmd = process.argv[2] || 'proxy';
3
+
4
+ const HELP = `openzoo — local x402-paying proxy + MCP server for openzoo.fun
5
+
6
+ usage:
7
+ npx openzoo start the proxy on http://localhost:8402/v1
8
+ npx openzoo mcp stdio MCP server (tools: zoo_ask, zoo_models, zoo_wallet)
9
+ npx openzoo demo ~1M-token needle demo: direct refuses, the zoo answers
10
+ npx openzoo balance wallet balances (yUSDCx / wTOKENx)
11
+ npx openzoo address print the funding address
12
+ npx openzoo help this text
13
+
14
+ point any OpenAI-compatible harness at:
15
+ base_url = http://localhost:8402/v1
16
+ api_key = sk-openzoo (any value; the zoo takes payment, not keys)
17
+
18
+ env:
19
+ OPENZOO_PORT (8402) OPENZOO_API_BASE (https://x402-tokens.fly.dev)
20
+ OPENZOO_RPC (mainnet-beta) OPENZOO_TOKEN (yUSDCx) OPENZOO_WALLET (~/.openzoo/wallet.json)
21
+ OPENZOO_MAX_USD_PER_CALL (0.5) OPENZOO_DEMO_MAX_USD (0.01)
22
+ OPENZOO_ENABLE_RH (0 — Robinhood Chain rail, experimental)`;
23
+
24
+ async function main() {
25
+ switch (cmd) {
26
+ case 'proxy':
27
+ case 'start':
28
+ await (await import('../lib/proxy.js')).startProxy();
29
+ break;
30
+ case 'mcp':
31
+ await (await import('../lib/mcp.js')).startMcp();
32
+ break;
33
+ case 'demo':
34
+ await (await import('../lib/demo.js')).runDemo();
35
+ break;
36
+ case 'balance':
37
+ await (await import('../lib/info.js')).printBalance();
38
+ break;
39
+ case 'address':
40
+ (await import('../lib/info.js')).printAddress();
41
+ break;
42
+ case 'help':
43
+ case '--help':
44
+ case '-h':
45
+ console.log(HELP);
46
+ break;
47
+ default:
48
+ console.error(`unknown command: ${cmd}\n`);
49
+ console.log(HELP);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ main().catch((err) => {
55
+ console.error(`openzoo: ${err.message}`);
56
+ process.exit(1);
57
+ });
package/lib/config.js ADDED
@@ -0,0 +1,15 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export const config = {
5
+ port: Number(process.env.OPENZOO_PORT || 8402),
6
+ apiBase: (process.env.OPENZOO_API_BASE || 'https://x402-tokens.fly.dev').replace(/\/+$/, ''),
7
+ rpcUrl: process.env.OPENZOO_RPC || 'https://api.mainnet-beta.solana.com',
8
+ // Which accepts[] row to pay with, by extra.symbol. yUSDCx is the $1-stable rail.
9
+ token: process.env.OPENZOO_TOKEN || 'yUSDCx',
10
+ walletPath: process.env.OPENZOO_WALLET || path.join(os.homedir(), '.openzoo', 'wallet.json'),
11
+ // Refuse to auto-pay any single 402 quote above this many USD (extra.billedUsd).
12
+ maxUsdPerCall: Number(process.env.OPENZOO_MAX_USD_PER_CALL || 0.5),
13
+ // Demo will only actually pay if the quote is at or below this.
14
+ demoMaxUsd: Number(process.env.OPENZOO_DEMO_MAX_USD || 0.01),
15
+ };
package/lib/demo.js ADDED
@@ -0,0 +1,104 @@
1
+ import { config } from './config.js';
2
+ import { PayClient } from './pay.js';
3
+ import { parse402, pickAccept, tokenBalance } from './x402.js';
4
+
5
+ const DEMO_MODEL = process.env.OPENZOO_DEMO_MODEL || 'nvidia/nemotron-3.5-lightning';
6
+ const NEEDLE = 'The zebra vault code is 7-ALPHA-9243.';
7
+ const QUESTION = 'Somewhere in the document above there is a zebra vault code. What is it? Answer with just the code.';
8
+
9
+ /** ~4 chars/token synthetic prose, distinct per paragraph (the zoo dedupes identical chunks). */
10
+ function bigBody(targetTokens) {
11
+ const targetChars = targetTokens * 4;
12
+ const parts = [];
13
+ let chars = 0;
14
+ let i = 0;
15
+ const needleAt = Math.floor(targetChars * 0.7);
16
+ let planted = false;
17
+ while (chars < targetChars) {
18
+ const p = `Field report ${i}: sector ${i % 977} logged a routine survey at hour ${i % 24}. `
19
+ + `Instrument ${((i * 7919) % 65536).toString(16)} recorded nominal telemetry; archivist note ${i * 31 % 10007} `
20
+ + `filed under shelf ${i % 431}. Nothing unusual was observed during pass ${i}.`;
21
+ parts.push(p);
22
+ chars += p.length + 2;
23
+ if (!planted && chars >= needleAt) { parts.push(NEEDLE); planted = true; }
24
+ i += 1;
25
+ }
26
+ if (!planted) parts.push(NEEDLE);
27
+ return parts.join('\n\n');
28
+ }
29
+
30
+ async function quoteFor(bodyObj) {
31
+ const r = await fetch(`${config.apiBase}/v1/chat/completions`, {
32
+ method: 'POST',
33
+ headers: { 'content-type': 'application/json' },
34
+ body: JSON.stringify(bodyObj),
35
+ });
36
+ if (r.status !== 402) throw new Error(`expected 402 quote, got HTTP ${r.status}: ${(await r.text()).slice(0, 300)}`);
37
+ return parse402(await r.json());
38
+ }
39
+
40
+ export async function runDemo() {
41
+ const client = new PayClient();
42
+ const { keypair, connection } = client;
43
+ const pub = client.address;
44
+
45
+ const tokens = Number(process.env.OPENZOO_DEMO_TOKENS || 965000);
46
+ console.log(`openzoo demo — model ${DEMO_MODEL}, ~${Math.round(tokens / 1000)}k-token body, one planted fact.`);
47
+ console.log('');
48
+ console.log('1) buying direct: a body this size does not run at all. OpenRouter returns:');
49
+ console.log(' "The input token count exceeds the maximum number of tokens allowed."');
50
+ console.log(' (recorded against the same model class, 2026-08-13 — benches.openzoo.fun)');
51
+ console.log('');
52
+
53
+ console.log('2) same body through the zoo — asking for the live x402 quote (the quote is free)...');
54
+ const doc = bigBody(tokens);
55
+ const bodyObj = {
56
+ model: DEMO_MODEL,
57
+ messages: [{ role: 'user', content: `${doc}\n\n${QUESTION}` }],
58
+ max_tokens: 32,
59
+ temperature: 0,
60
+ };
61
+ const quote = await quoteFor(bodyObj);
62
+ const accept = pickAccept(quote, config.token);
63
+ const x = accept.extra || {};
64
+ console.log(` quote: $${Number(x.billedUsd).toFixed(6)} ${x.symbol} · pricing=${x.pricing}`
65
+ + (x.directUsd != null ? ` · direct would be $${Number(x.directUsd).toFixed(6)} · savesVsDirect=${Number(x.savesVsDirect).toFixed(1)}×` : ''));
66
+ console.log('');
67
+
68
+ // Decide what we can actually pay for under the demo cap.
69
+ let payBody = bodyObj;
70
+ let payAccept = accept;
71
+ if (Number(x.billedUsd) > config.demoMaxUsd) {
72
+ const fitTokens = Math.max(20000, Math.floor(tokens * (config.demoMaxUsd / Number(x.billedUsd)) * 0.8));
73
+ console.log(` (full-size quote is over the demo cap $${config.demoMaxUsd} — paying for a ${Math.round(fitTokens / 1000)}k-token slice instead;`);
74
+ console.log(' set OPENZOO_DEMO_MAX_USD higher to pay for the whole thing)');
75
+ const doc2 = bigBody(fitTokens);
76
+ payBody = { ...bodyObj, messages: [{ role: 'user', content: `${doc2}\n\n${QUESTION}` }] };
77
+ const q2 = await quoteFor(payBody);
78
+ payAccept = pickAccept(q2, config.token);
79
+ }
80
+
81
+ const need = BigInt(payAccept.maxAmountRequired);
82
+ const bal = await tokenBalance(connection, keypair.publicKey, payAccept.asset);
83
+ if (bal.raw < need) {
84
+ console.log('3) wallet not funded yet, so stopping before payment.');
85
+ console.log(` need : ${need} raw units of ${payAccept.extra?.symbol} (mint ${payAccept.asset}) ≈ $${Number(payAccept.extra?.billedUsd).toFixed(6)}`);
86
+ console.log(` have : ${bal.raw}`);
87
+ console.log(` fund : ${pub}`);
88
+ console.log(' get yUSDCx by wrapping USDC: https://x402.accrue.fund/start — then re-run `npx openzoo demo`.');
89
+ return;
90
+ }
91
+
92
+ console.log('3) paying and asking...');
93
+ const t0 = Date.now();
94
+ const { data, receipt } = await client.chat(payBody);
95
+ const secs = ((Date.now() - t0) / 1000).toFixed(1);
96
+ const answer = data?.choices?.[0]?.message?.content ?? '';
97
+ const usage = data?.usage || {};
98
+ console.log(` answer (${secs}s): ${answer.trim()}`);
99
+ console.log(` verdict: ${answer.includes('9243') ? 'HIT — the planted fact came back' : 'MISS'}`);
100
+ if (usage.gpu_tokens != null || usage.prompt_tokens != null) {
101
+ console.log(` tokens the model actually read: ${usage.gpu_tokens ?? usage.prompt_tokens}${usage.spill_tokens ? ` (spilled: ${usage.spill_tokens})` : ''}`);
102
+ }
103
+ if (receipt) console.log(` ${receipt.line}`);
104
+ }
package/lib/evm.js ADDED
@@ -0,0 +1,77 @@
1
+ import crypto from 'node:crypto';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+ import { evmChainId } from './x402.js';
4
+
5
+ /**
6
+ * EVM rails (Base eip155:8453; Robinhood Chain eip155:4663 behind OPENZOO_ENABLE_RH).
7
+ *
8
+ * Standard x402 v1 EVM "exact" scheme: an EIP-3009 transferWithAuthorization
9
+ * typed-data signature; the facilitator submits it. X-PAYMENT = base64 of
10
+ * { x402Version: 1, scheme: "exact", network: "<as offered>",
11
+ * payload: { signature, authorization: { from, to, value, validAfter, validBefore, nonce } } }
12
+ *
13
+ * IMPORTANT — the decimals landmine does not exist here BY CONSTRUCTION:
14
+ * authorization.value is the raw uint256 straight from maxAmountRequired.
15
+ * We never scale by decimals, so the zoo's /prompt.txt "decimals = 6" bug
16
+ * (wrong for the 18-decimal Robinhood mints) cannot bite this path.
17
+ *
18
+ * STATUS: code-present, live-UNTESTED — the zoo's 402s currently offer only
19
+ * Solana rows, and whether its facilitator settles eip155 chains at all is
20
+ * unverified upstream.
21
+ */
22
+ export async function buildEvmPayment({ accept, evmPrivateKey }) {
23
+ const chainId = evmChainId(accept.network);
24
+ if (!chainId) throw new Error(`cannot parse EVM chain id from network ${accept.network}`);
25
+ const account = privateKeyToAccount(evmPrivateKey);
26
+
27
+ const now = Math.floor(Date.now() / 1000);
28
+ const authorization = {
29
+ from: account.address,
30
+ to: accept.payTo,
31
+ value: BigInt(accept.maxAmountRequired), // raw units from the 402 — never decimal-scaled
32
+ validAfter: 0n,
33
+ validBefore: BigInt(now + (accept.maxTimeoutSeconds || 600)),
34
+ nonce: `0x${crypto.randomBytes(32).toString('hex')}`,
35
+ };
36
+
37
+ const signature = await account.signTypedData({
38
+ domain: {
39
+ name: accept.extra?.name || 'USDC',
40
+ version: accept.extra?.version || '2',
41
+ chainId,
42
+ verifyingContract: accept.asset,
43
+ },
44
+ types: {
45
+ TransferWithAuthorization: [
46
+ { name: 'from', type: 'address' },
47
+ { name: 'to', type: 'address' },
48
+ { name: 'value', type: 'uint256' },
49
+ { name: 'validAfter', type: 'uint256' },
50
+ { name: 'validBefore', type: 'uint256' },
51
+ { name: 'nonce', type: 'bytes32' },
52
+ ],
53
+ },
54
+ primaryType: 'TransferWithAuthorization',
55
+ message: authorization,
56
+ });
57
+
58
+ const envelope = {
59
+ x402Version: 1,
60
+ scheme: accept.scheme,
61
+ network: accept.network,
62
+ payload: {
63
+ signature,
64
+ authorization: {
65
+ ...authorization,
66
+ value: authorization.value.toString(),
67
+ validAfter: authorization.validAfter.toString(),
68
+ validBefore: authorization.validBefore.toString(),
69
+ },
70
+ },
71
+ };
72
+ return {
73
+ header: Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64'),
74
+ ownerSignature: signature,
75
+ from: account.address,
76
+ };
77
+ }
package/lib/info.js ADDED
@@ -0,0 +1,30 @@
1
+ import { Connection } from '@solana/web3.js';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+ import { config } from './config.js';
4
+ import { loadOrCreateWallet } from './wallet.js';
5
+ import { tokenBalance } from './x402.js';
6
+
7
+ const YUSDCX = '6ZjjxcoicqM4nniddkuPVwew4PDwY3swbfHsGbCuLuTv';
8
+ const WTOKENX = 'Bo7xBF7SY8EyUBPUxRP66SFafxoPf2n5uqiLjbxEebx9';
9
+
10
+ export function printAddress() {
11
+ const { keypair, evmPrivateKey, created, path } = loadOrCreateWallet();
12
+ if (created) console.log(`new burner wallet created at ${path} (chmod 600)`);
13
+ console.log(keypair.publicKey.toBase58());
14
+ console.log(`(evm, for Base/RH rails — untested: ${privateKeyToAccount(evmPrivateKey).address})`);
15
+ }
16
+
17
+ export async function printBalance() {
18
+ const { keypair } = loadOrCreateWallet();
19
+ const connection = new Connection(config.rpcUrl, 'confirmed');
20
+ const [y, w] = await Promise.all([
21
+ tokenBalance(connection, keypair.publicKey, YUSDCX),
22
+ tokenBalance(connection, keypair.publicKey, WTOKENX),
23
+ ]);
24
+ console.log(`wallet : ${keypair.publicKey.toBase58()}`);
25
+ console.log(`yUSDCx : ${y.ui ?? 0} (mint ${YUSDCX})`);
26
+ console.log(`wTOKENx: ${w.ui ?? 0} (mint ${WTOKENX})`);
27
+ if (!y.raw && !w.raw) {
28
+ console.log('unfunded — wrap USDC into yUSDCx at https://x402.accrue.fund/start and send it here.');
29
+ }
30
+ }
package/lib/mcp.js ADDED
@@ -0,0 +1,90 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { config } from './config.js';
5
+ import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
6
+ import { tokenBalance } from './x402.js';
7
+
8
+ const YUSDCX = '6ZjjxcoicqM4nniddkuPVwew4PDwY3swbfHsGbCuLuTv';
9
+ const WTOKENX = 'Bo7xBF7SY8EyUBPUxRP66SFafxoPf2n5uqiLjbxEebx9';
10
+ const DEFAULT_MODEL = process.env.OPENZOO_DEMO_MODEL || 'nvidia/nemotron-3.5-lightning';
11
+
12
+ function text(obj) {
13
+ return { content: [{ type: 'text', text: typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2) }] };
14
+ }
15
+
16
+ /** `npx openzoo mcp` — stdio MCP server sharing the proxy's wallet + payment core. */
17
+ export async function startMcp() {
18
+ const client = new PayClient();
19
+ const server = new McpServer({ name: 'openzoo', version: '0.1.0' });
20
+
21
+ server.registerTool('zoo_ask', {
22
+ description:
23
+ 'Ask a question through openzoo.fun, paying the x402 quote transparently from the local burner wallet. '
24
+ + 'The zoo puts leCore holographic memory in front of every model, so `corpus` can be a HUGE text body '
25
+ + '(hundreds of thousands to ~1M tokens) that the model itself would refuse — the zoo spills it and the '
26
+ + 'model reads only a few thousand tokens. Returns the answer plus a payment receipt '
27
+ + '(billedUsd, savesVsDirect, tokens actually read).',
28
+ inputSchema: {
29
+ prompt: z.string().describe('The question or instruction.'),
30
+ corpus: z.string().optional().describe('Optional big context body (document dump, logs, book...). Placed before the prompt.'),
31
+ model: z.string().optional().describe(`Model id from zoo_models. Default ${DEFAULT_MODEL}.`),
32
+ max_tokens: z.number().int().positive().optional().describe('Completion cap. Default 1024.'),
33
+ },
34
+ }, async ({ prompt, corpus, model, max_tokens }) => {
35
+ const content = corpus ? `${corpus}\n\n${prompt}` : prompt;
36
+ try {
37
+ const { data, receipt } = await client.chat({
38
+ model: model || DEFAULT_MODEL,
39
+ messages: [{ role: 'user', content }],
40
+ max_tokens: max_tokens || 1024,
41
+ });
42
+ const usage = data?.usage || {};
43
+ return text({
44
+ answer: data?.choices?.[0]?.message?.content ?? '',
45
+ receipt: receipt ? {
46
+ line: receipt.line,
47
+ billedUsd: receipt.billedUsd,
48
+ savesVsDirect: receipt.savesVsDirect,
49
+ pricing: receipt.pricing,
50
+ rail: receipt.rail,
51
+ } : 'free (no 402 issued)',
52
+ tokensRead: usage.gpu_tokens ?? usage.prompt_tokens ?? null,
53
+ spillTokens: usage.spill_tokens ?? null,
54
+ });
55
+ } catch (err) {
56
+ if (err instanceof UnderfundedError || err instanceof QuoteTooHighError) {
57
+ return { ...text(err.message), isError: true };
58
+ }
59
+ throw err;
60
+ }
61
+ });
62
+
63
+ server.registerTool('zoo_models', {
64
+ description: 'List the models the zoo serves, with per-token pricing (free endpoint, no payment).',
65
+ inputSchema: {},
66
+ }, async () => {
67
+ const r = await fetch(`${config.apiBase}/v1/models`);
68
+ return text(await r.json());
69
+ });
70
+
71
+ server.registerTool('zoo_wallet', {
72
+ description: 'The local burner wallet: funding address, balances, and this session\'s payment receipts.',
73
+ inputSchema: {},
74
+ }, async () => {
75
+ const [y, w] = await Promise.all([
76
+ tokenBalance(client.connection, client.keypair.publicKey, YUSDCX),
77
+ tokenBalance(client.connection, client.keypair.publicKey, WTOKENX),
78
+ ]);
79
+ return text({
80
+ solanaAddress: client.address,
81
+ fundWith: `yUSDCx (mint ${YUSDCX}) — wrap USDC at https://x402.accrue.fund/start`,
82
+ balances: { yUSDCx: y.ui ?? 0, wTOKENx: w.ui ?? 0 },
83
+ receipts: client.receipts.map((r) => ({ at: r.at, line: r.line })),
84
+ });
85
+ });
86
+
87
+ const transport = new StdioServerTransport();
88
+ await server.connect(transport);
89
+ console.error(`openzoo mcp on stdio — wallet ${client.address} — zoo ${config.apiBase}`);
90
+ }
package/lib/pay.js ADDED
@@ -0,0 +1,116 @@
1
+ import { Connection } from '@solana/web3.js';
2
+ import { config } from './config.js';
3
+ import { loadOrCreateWallet } from './wallet.js';
4
+ import {
5
+ parse402, pickAccept, railOf, buildPaymentOnline, tokenBalance,
6
+ receiptLine, decodeSettleHeader,
7
+ } from './x402.js';
8
+ import { buildEvmPayment } from './evm.js';
9
+
10
+ export class QuoteTooHighError extends Error {
11
+ constructor(billedUsd, quote) {
12
+ super(`openzoo: quote $${billedUsd} exceeds local cap $${config.maxUsdPerCall}. Raise OPENZOO_MAX_USD_PER_CALL to allow.`);
13
+ this.billedUsd = billedUsd;
14
+ this.quote = quote;
15
+ }
16
+ }
17
+
18
+ export class UnderfundedError extends Error {
19
+ constructor(accept, have, address) {
20
+ super(`openzoo wallet underfunded: need ${accept.maxAmountRequired} raw ${accept.extra?.symbol || ''} (mint ${accept.asset}), have ${have}. Fund ${address}`);
21
+ this.accept = accept;
22
+ this.have = have;
23
+ this.address = address;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Shared x402-paying HTTP client — the single place payment happens.
29
+ * The proxy, the demo, and the MCP server all go through PayClient.fetch:
30
+ * request → 402 → pick rail (Solana first) → sign → retry with X-PAYMENT.
31
+ */
32
+ export class PayClient {
33
+ constructor() {
34
+ const w = loadOrCreateWallet();
35
+ this.keypair = w.keypair;
36
+ this.evmPrivateKey = w.evmPrivateKey;
37
+ this.walletCreated = w.created;
38
+ this.walletPath = w.path;
39
+ this.connection = new Connection(config.rpcUrl, 'confirmed');
40
+ this.receipts = []; // last paid calls, newest last
41
+ this.allowRH = process.env.OPENZOO_ENABLE_RH === '1';
42
+ }
43
+
44
+ get address() { return this.keypair.publicKey.toBase58(); }
45
+
46
+ async buildPaymentFor(accept) {
47
+ const rail = railOf(accept);
48
+ if (rail === 'solana') {
49
+ const bal = await tokenBalance(this.connection, this.keypair.publicKey, accept.asset);
50
+ if (bal.raw < BigInt(accept.maxAmountRequired)) {
51
+ throw new UnderfundedError(accept, bal.raw, this.address);
52
+ }
53
+ return buildPaymentOnline(this.connection, this.keypair, accept);
54
+ }
55
+ if (rail === 'base' || rail === 'evm' || (rail === 'robinhood' && this.allowRH)) {
56
+ return buildEvmPayment({ accept, evmPrivateKey: this.evmPrivateKey });
57
+ }
58
+ throw new Error(`no payment builder for rail ${rail} (network ${accept.network})`);
59
+ }
60
+
61
+ /**
62
+ * fetch that transparently pays a 402 once.
63
+ * Returns { response, paid, accept?, settle?, receipt? }.
64
+ * Throws QuoteTooHighError / UnderfundedError before any value moves.
65
+ */
66
+ async fetch(url, init = {}) {
67
+ const first = await fetch(url, init);
68
+ if (first.status !== 402) return { response: first, paid: false };
69
+
70
+ const quote = parse402(await first.json());
71
+ const accept = pickAccept(quote, config.token, { allowRH: this.allowRH });
72
+ const billedUsd = Number(accept?.extra?.billedUsd ?? NaN);
73
+ if (Number.isFinite(billedUsd) && billedUsd > config.maxUsdPerCall) {
74
+ throw new QuoteTooHighError(billedUsd, quote);
75
+ }
76
+
77
+ const payment = await this.buildPaymentFor(accept);
78
+ const response = await fetch(url, {
79
+ ...init,
80
+ headers: { ...(init.headers || {}), 'X-PAYMENT': payment.header },
81
+ });
82
+ const settle = decodeSettleHeader(response.headers.get('x-payment-response'))
83
+ || { signature: payment.ownerSignature };
84
+ const receipt = {
85
+ at: new Date().toISOString(),
86
+ line: receiptLine(accept, settle),
87
+ rail: railOf(accept),
88
+ billedUsd: accept.extra?.billedUsd,
89
+ savesVsDirect: accept.extra?.savesVsDirect,
90
+ pricing: accept.extra?.pricing,
91
+ symbol: accept.extra?.symbol,
92
+ amountRaw: accept.maxAmountRequired,
93
+ ok: response.ok,
94
+ status: response.status,
95
+ };
96
+ if (response.ok) {
97
+ this.receipts.push(receipt);
98
+ if (this.receipts.length > 20) this.receipts.shift();
99
+ }
100
+ return { response, paid: true, accept, settle, receipt };
101
+ }
102
+
103
+ /** POST a chat completion, paying as needed. Returns { data, receipt }. */
104
+ async chat(bodyObj) {
105
+ const { response, receipt } = await this.fetch(`${config.apiBase}/v1/chat/completions`, {
106
+ method: 'POST',
107
+ headers: { 'content-type': 'application/json' },
108
+ body: JSON.stringify(bodyObj),
109
+ });
110
+ if (!response.ok) {
111
+ const text = (await response.text()).slice(0, 500);
112
+ throw new Error(`zoo returned HTTP ${response.status}: ${text}`);
113
+ }
114
+ return { data: await response.json(), receipt };
115
+ }
116
+ }
package/lib/proxy.js ADDED
@@ -0,0 +1,110 @@
1
+ import http from 'node:http';
2
+ import { Readable } from 'node:stream';
3
+ import { config } from './config.js';
4
+ import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
5
+ import { tokenBalance } from './x402.js';
6
+
7
+ const HOP_BY_HOP = new Set([
8
+ 'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
9
+ 'proxy-authorization', 'proxy-connection', 'te', 'trailer', 'content-length',
10
+ // The harness's api key (sk-openzoo or anything) is accepted and dropped:
11
+ // the zoo takes payment, not keys.
12
+ 'authorization',
13
+ ]);
14
+
15
+ function upstreamHeaders(req) {
16
+ const out = {};
17
+ for (const [k, v] of Object.entries(req.headers)) {
18
+ if (!HOP_BY_HOP.has(k.toLowerCase())) out[k] = v;
19
+ }
20
+ return out;
21
+ }
22
+
23
+ async function readBody(req) {
24
+ const chunks = [];
25
+ for await (const c of req) chunks.push(c);
26
+ return Buffer.concat(chunks);
27
+ }
28
+
29
+ /** Pipe an upstream fetch Response to the client, unbuffered (SSE-safe). */
30
+ function relay(res, upstream) {
31
+ const headers = {};
32
+ upstream.headers.forEach((v, k) => {
33
+ if (!['transfer-encoding', 'connection', 'content-encoding', 'content-length'].includes(k)) headers[k] = v;
34
+ });
35
+ res.writeHead(upstream.status, headers);
36
+ if (!upstream.body) { res.end(); return Promise.resolve(); }
37
+ return new Promise((resolve) => {
38
+ const body = Readable.fromWeb(upstream.body);
39
+ body.on('error', () => res.destroy());
40
+ res.on('close', () => body.destroy());
41
+ body.on('end', resolve);
42
+ body.pipe(res);
43
+ });
44
+ }
45
+
46
+ function jsonErr(res, status, message, extraFields = {}) {
47
+ res.writeHead(status, { 'content-type': 'application/json' });
48
+ res.end(JSON.stringify({ error: { message }, ...extraFields }));
49
+ }
50
+
51
+ export async function startProxy({ silent = false } = {}) {
52
+ const client = new PayClient();
53
+ const log = silent ? () => {} : (...a) => console.log(...a);
54
+
55
+ const server = http.createServer(async (req, res) => {
56
+ const url = `${config.apiBase}${req.url}`;
57
+ let bodyBuf;
58
+ try {
59
+ bodyBuf = await readBody(req);
60
+ } catch {
61
+ jsonErr(res, 400, 'bad request body');
62
+ return;
63
+ }
64
+ const init = { method: req.method, headers: upstreamHeaders(req) };
65
+ if (req.method !== 'GET' && req.method !== 'HEAD') init.body = bodyBuf;
66
+
67
+ try {
68
+ const { response, paid, receipt } = await client.fetch(url, init);
69
+ if (paid && receipt) log(receipt.ok ? receipt.line : `paid retry -> HTTP ${receipt.status}`);
70
+ await relay(res, response);
71
+ } catch (err) {
72
+ if (err instanceof QuoteTooHighError) {
73
+ log(err.message);
74
+ jsonErr(res, 402, err.message, { quote: err.quote });
75
+ } else if (err instanceof UnderfundedError) {
76
+ log(err.message);
77
+ jsonErr(res, 402, err.message);
78
+ } else {
79
+ jsonErr(res, 502, `openzoo proxy error: ${err.message}`);
80
+ }
81
+ }
82
+ });
83
+
84
+ await new Promise((resolve, reject) => {
85
+ server.on('error', reject);
86
+ server.listen(config.port, '127.0.0.1', resolve);
87
+ });
88
+
89
+ if (!silent) {
90
+ console.log(`openzoo proxy -> ${config.apiBase}`);
91
+ console.log(`listening on http://localhost:${config.port}/v1`);
92
+ console.log('');
93
+ if (client.walletCreated) console.log(`new burner wallet created at ${client.walletPath} (chmod 600)`);
94
+ console.log(`wallet (fund me): ${client.address}`);
95
+ try {
96
+ // yUSDCx — the $1-stable rail this proxy pays with by default.
97
+ const yusdcx = '6ZjjxcoicqM4nniddkuPVwew4PDwY3swbfHsGbCuLuTv';
98
+ const bal = await tokenBalance(client.connection, client.keypair.publicKey, yusdcx);
99
+ console.log(`balance: ${bal.ui ?? 0} yUSDCx (mint ${yusdcx})`);
100
+ if (!bal.raw) {
101
+ console.log('fund it with yUSDCx (wrap USDC at https://x402.accrue.fund/start) — a few cents goes a long way.');
102
+ }
103
+ } catch { /* RPC hiccup: balance is advisory */ }
104
+ console.log('');
105
+ console.log('point any OpenAI-compatible harness at:');
106
+ console.log(` base_url = http://localhost:${config.port}/v1`);
107
+ console.log(' api_key = sk-openzoo (any value works; the zoo takes payment, not keys)');
108
+ }
109
+ return { server, client };
110
+ }
package/lib/wallet.js ADDED
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { Keypair } from '@solana/web3.js';
5
+ import { config } from './config.js';
6
+
7
+ /**
8
+ * Local burner wallet at ~/.openzoo/wallet.json (chmod 600).
9
+ * Format v2: { "solana": [64 secret-key bytes], "evm": "0x<32-byte hex>" }
10
+ * Legacy format (bare 64-byte array, solana-keygen style) is migrated in place.
11
+ * Your keys never leave this machine — the zoo only ever sees signed transfers.
12
+ */
13
+ export function loadOrCreateWallet() {
14
+ const p = config.walletPath;
15
+ let solanaBytes;
16
+ let evmKey;
17
+ let created = false;
18
+
19
+ if (fs.existsSync(p)) {
20
+ const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
21
+ if (Array.isArray(raw)) {
22
+ solanaBytes = Uint8Array.from(raw); // legacy: migrate below by adding an EVM key
23
+ } else {
24
+ solanaBytes = Uint8Array.from(raw.solana);
25
+ evmKey = raw.evm;
26
+ }
27
+ } else {
28
+ solanaBytes = Keypair.generate().secretKey;
29
+ created = true;
30
+ }
31
+ if (!evmKey) {
32
+ evmKey = `0x${crypto.randomBytes(32).toString('hex')}`;
33
+ fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
34
+ fs.writeFileSync(p, JSON.stringify({ solana: Array.from(solanaBytes), evm: evmKey }), { mode: 0o600 });
35
+ }
36
+ return {
37
+ keypair: Keypair.fromSecretKey(solanaBytes),
38
+ evmPrivateKey: evmKey,
39
+ created,
40
+ path: p,
41
+ };
42
+ }
package/lib/x402.js ADDED
@@ -0,0 +1,188 @@
1
+ import {
2
+ Connection,
3
+ PublicKey,
4
+ Transaction,
5
+ } from '@solana/web3.js';
6
+ import {
7
+ TOKEN_2022_PROGRAM_ID,
8
+ createTransferCheckedInstruction,
9
+ getAssociatedTokenAddressSync,
10
+ unpackMint,
11
+ } from '@solana/spl-token';
12
+
13
+ /**
14
+ * x402 protocol client for openzoo.fun (x402Version 1, Solana "exact" scheme).
15
+ *
16
+ * Captured live 402 shape (test/fixtures/live-402.json):
17
+ * { x402Version: 1,
18
+ * accepts: [ { scheme: "exact",
19
+ * network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
20
+ * asset: "<mint>", maxAmountRequired: "<raw units, string>",
21
+ * payTo: "<wallet>", resource, description, maxTimeoutSeconds,
22
+ * extra: { facilitator, feePayer, symbol, billedUsd, tokenUsd,
23
+ * pricedAt, pricing: "markup"|"counterfactual",
24
+ * markup? , directUsd?, savesVsDirect? } } ],
25
+ * error: "payment required", help: "..." }
26
+ *
27
+ * Payment: ONE Token-2022 TransferChecked (payer ATA -> payTo ATA) for exactly
28
+ * maxAmountRequired, feePayer = extra.feePayer (the gateway pays SOL fees),
29
+ * partial-signed by the payer, serialized requireAllSignatures=false, base64.
30
+ * X-PAYMENT header = base64 of
31
+ * {"x402Version":1,"scheme":"exact","network":"<network>","payload":{"transaction":"<b64 tx>"}}
32
+ *
33
+ * NOTE: decimals are NOT in the 402 payload and the site's pasted prompt
34
+ * hardcoding "decimals = 6" is a known bug. We always read decimals from the
35
+ * mint account on-chain (see getMintInfo) — never hardcode.
36
+ */
37
+
38
+ export function parse402(body) {
39
+ if (!body || body.x402Version !== 1 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
40
+ throw new Error('not an x402 v1 payment-required body');
41
+ }
42
+ return body;
43
+ }
44
+
45
+ const EVM_NAME_CHAINS = { base: 8453, 'base-sepolia': 84532 };
46
+
47
+ /** Which rail an accepts[] row settles on: 'solana' | 'base' | 'robinhood' | 'evm' | null. */
48
+ export function railOf(accept) {
49
+ const net = accept?.network || '';
50
+ if (net.startsWith('solana:')) return 'solana';
51
+ const m = /^eip155:(\d+)$/.exec(net);
52
+ const chainId = m ? Number(m[1]) : EVM_NAME_CHAINS[net];
53
+ if (chainId === 8453 || chainId === 84532) return 'base';
54
+ if (chainId === 4663) return 'robinhood';
55
+ if (chainId) return 'evm';
56
+ return null;
57
+ }
58
+
59
+ export function evmChainId(network) {
60
+ const m = /^eip155:(\d+)$/.exec(network || '');
61
+ return m ? Number(m[1]) : EVM_NAME_CHAINS[network] ?? null;
62
+ }
63
+
64
+ /**
65
+ * Choose the accepts[] row to pay with. Rail order: Solana first (preferring
66
+ * extra.symbol === preferredSymbol), then Base, then other EVM chains.
67
+ * Robinhood Chain (eip155:4663) only when allowRH — the zoo ships it dark
68
+ * (RH_RAILS=0) and facilitator settlement there is unverified.
69
+ */
70
+ export function pickAccept(body, preferredSymbol, { allowRH = false } = {}) {
71
+ const rows = parse402(body).accepts.filter((a) => a?.scheme === 'exact');
72
+ const sol = rows.filter((a) => railOf(a) === 'solana');
73
+ if (sol.length) return sol.find((a) => a?.extra?.symbol === preferredSymbol) || sol[0];
74
+ const base = rows.filter((a) => railOf(a) === 'base');
75
+ if (base.length) return base[0];
76
+ const evm = rows.filter((a) => railOf(a) === 'evm');
77
+ if (evm.length) return evm[0];
78
+ const rh = rows.filter((a) => railOf(a) === 'robinhood');
79
+ if (allowRH && rh.length) return rh[0];
80
+ throw new Error(rh.length
81
+ ? 'only Robinhood Chain rails offered — set OPENZOO_ENABLE_RH=1 to use them (experimental, settlement unverified)'
82
+ : 'no payable rail in 402 accepts[]');
83
+ }
84
+
85
+ const mintCache = new Map();
86
+
87
+ /** Read the mint's owner program and decimals from chain. Cached per mint. */
88
+ export async function getMintInfo(connection, mintStr) {
89
+ if (mintCache.has(mintStr)) return mintCache.get(mintStr);
90
+ const mint = new PublicKey(mintStr);
91
+ const info = await connection.getAccountInfo(mint);
92
+ if (!info) throw new Error(`mint ${mintStr} not found on chain (check OPENZOO_RPC)`);
93
+ const programId = info.owner; // TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID
94
+ const parsed = unpackMint(mint, info, programId);
95
+ const out = { programId, decimals: parsed.decimals };
96
+ mintCache.set(mintStr, out);
97
+ return out;
98
+ }
99
+
100
+ /**
101
+ * Build the partially-signed payment transaction + X-PAYMENT header value.
102
+ * Pure given its inputs (no network) so it can be unit-tested offline.
103
+ */
104
+ export function buildPayment({ accept, decimals, programId, recentBlockhash, keypair }) {
105
+ const mint = new PublicKey(accept.asset);
106
+ const payTo = new PublicKey(accept.payTo);
107
+ const feePayer = new PublicKey(accept.extra.feePayer);
108
+ const amount = BigInt(accept.maxAmountRequired);
109
+
110
+ const source = getAssociatedTokenAddressSync(mint, keypair.publicKey, false, programId);
111
+ const dest = getAssociatedTokenAddressSync(mint, payTo, true, programId);
112
+
113
+ const ix = createTransferCheckedInstruction(
114
+ source, mint, dest, keypair.publicKey, amount, decimals, [], programId,
115
+ );
116
+
117
+ const tx = new Transaction({ feePayer, recentBlockhash });
118
+ tx.add(ix);
119
+ tx.partialSign(keypair); // owner signs; feePayer slot left empty for the facilitator
120
+ const txBase64 = tx.serialize({ requireAllSignatures: false, verifySignatures: false }).toString('base64');
121
+
122
+ const envelope = {
123
+ x402Version: 1,
124
+ scheme: accept.scheme,
125
+ network: accept.network,
126
+ payload: { transaction: txBase64 },
127
+ };
128
+ const header = Buffer.from(JSON.stringify(envelope), 'utf8').toString('base64');
129
+ const ownerSig = tx.signatures.find((s) => s.publicKey.equals(keypair.publicKey) && s.signature);
130
+ return {
131
+ header,
132
+ txBase64,
133
+ ownerSignature: ownerSig ? base58encode(ownerSig.signature) : null,
134
+ amount,
135
+ source,
136
+ dest,
137
+ };
138
+ }
139
+
140
+ /** Resolve chain state (decimals, blockhash) and build the header. */
141
+ export async function buildPaymentOnline(connection, keypair, accept) {
142
+ const { programId, decimals } = await getMintInfo(connection, accept.asset);
143
+ const { blockhash } = await connection.getLatestBlockhash('confirmed');
144
+ return buildPayment({ accept, decimals, programId, recentBlockhash: blockhash, keypair });
145
+ }
146
+
147
+ /** Token balance of our ATA for a mint. Returns { raw: bigint, ui: number|null }. */
148
+ export async function tokenBalance(connection, owner, mintStr) {
149
+ try {
150
+ const { programId } = await getMintInfo(connection, mintStr);
151
+ const ata = getAssociatedTokenAddressSync(new PublicKey(mintStr), owner, false, programId);
152
+ const bal = await connection.getTokenAccountBalance(ata);
153
+ return { raw: BigInt(bal.value.amount), ui: bal.value.uiAmount };
154
+ } catch {
155
+ return { raw: 0n, ui: 0 };
156
+ }
157
+ }
158
+
159
+ /** One-line human receipt for a paid call. */
160
+ export function receiptLine(accept, settle) {
161
+ const x = accept.extra || {};
162
+ const usd = x.billedUsd != null ? `$${Number(x.billedUsd).toFixed(6)}` : `${accept.maxAmountRequired} raw ${x.symbol || ''}`;
163
+ const saves = x.savesVsDirect != null ? ` (${Number(x.savesVsDirect).toFixed(1)}× cheaper than direct)` : (x.markup != null ? ` (markup ${x.markup}×, short body)` : '');
164
+ const tx = settle?.transaction || settle?.txHash || settle?.signature;
165
+ const rail = railOf(accept);
166
+ return `paid ${usd}${saves}${rail ? ` · rail ${rail}` : ''}${tx ? ` · tx ${tx}` : ''}`;
167
+ }
168
+
169
+ /** Decode the server's X-PAYMENT-RESPONSE header (base64 JSON), if present. */
170
+ export function decodeSettleHeader(headerValue) {
171
+ if (!headerValue) return null;
172
+ try {
173
+ return JSON.parse(Buffer.from(headerValue, 'base64').toString('utf8'));
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ // Minimal base58 (bitcoin alphabet) — enough to print a signature.
180
+ const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
181
+ function base58encode(bytes) {
182
+ let n = 0n;
183
+ for (const b of bytes) n = n * 256n + BigInt(b);
184
+ let out = '';
185
+ while (n > 0n) { out = B58[Number(n % 58n)] + out; n /= 58n; }
186
+ for (const b of bytes) { if (b === 0) out = '1' + out; else break; }
187
+ return out;
188
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "openzoo",
3
+ "version": "0.1.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.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "openzoo": "bin/openzoo.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "lib",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "test": "node --test test/*.test.js"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.12.0",
23
+ "@solana/spl-token": "^0.4.14",
24
+ "@solana/web3.js": "^1.98.4",
25
+ "viem": "^2.21.0",
26
+ "zod": "^3.24.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/staccDOTsol/openzoo.git"
31
+ },
32
+ "homepage": "https://openzoo.fun",
33
+ "keywords": [
34
+ "x402",
35
+ "openai",
36
+ "proxy",
37
+ "solana",
38
+ "llm",
39
+ "openzoo",
40
+ "payments"
41
+ ]
42
+ }