openzoo 0.9.1 → 0.9.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/lib/pay.js +35 -9
- package/lib/x402.js +30 -13
- package/package.json +1 -1
package/lib/pay.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getAssociatedTokenAddressSync } from '@solana/spl-token';
|
|
|
3
3
|
import { config, fundingLine } from './config.js';
|
|
4
4
|
import { loadOrCreateWallet } from './wallet.js';
|
|
5
5
|
import {
|
|
6
|
-
parse402,
|
|
6
|
+
parse402, orderAccepts, railOf, buildPaymentOnline, tokenBalance,
|
|
7
7
|
receiptLine, decodeSettleHeader,
|
|
8
8
|
} from './x402.js';
|
|
9
9
|
import { buildEvmPayment, evmTokenBalance } from './evm.js';
|
|
@@ -180,15 +180,41 @@ export class PayClient {
|
|
|
180
180
|
|
|
181
181
|
const quote = parse402(await first.json());
|
|
182
182
|
// config.rail (OPENZOO_RAIL) steers every front — proxy, demo, MCP — since
|
|
183
|
-
// they all pay through this one call site.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
183
|
+
// they all pay through this one call site. The wallet pays with whatever
|
|
184
|
+
// it HOLDS: every offered row is tried best-first, and only when NONE is
|
|
185
|
+
// affordable does the call fail — never because the first-choice asset
|
|
186
|
+
// alone ran dry while another funded one sat in the wallet.
|
|
187
|
+
const candidates = orderAccepts(quote, config.token, { allowRH: this.allowRH, forceRail: config.rail });
|
|
190
188
|
onStage?.('quoted');
|
|
191
|
-
|
|
189
|
+
let accept = null;
|
|
190
|
+
let payment = null;
|
|
191
|
+
const fundErrs = [];
|
|
192
|
+
let tooHigh = null;
|
|
193
|
+
for (const cand of candidates) {
|
|
194
|
+
const billedUsd = Number(cand?.extra?.billedUsd ?? NaN);
|
|
195
|
+
if (Number.isFinite(billedUsd) && billedUsd > config.maxUsdPerCall) {
|
|
196
|
+
tooHigh = tooHigh || new QuoteTooHighError(billedUsd, quote);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
payment = await this.buildPaymentFor(cand, onStage);
|
|
201
|
+
accept = cand;
|
|
202
|
+
break;
|
|
203
|
+
} catch (e) {
|
|
204
|
+
const funding = e instanceof UnderfundedError
|
|
205
|
+
|| e?.name === 'UnderlyingShortError' || e?.name === 'NeedsGasError';
|
|
206
|
+
if (!funding) throw e;
|
|
207
|
+
fundErrs.push({ sym: cand?.extra?.symbol || cand?.asset, err: e });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!payment) {
|
|
211
|
+
if (!fundErrs.length && tooHigh) throw tooHigh;
|
|
212
|
+
if (fundErrs.length === 1) throw fundErrs[0].err;
|
|
213
|
+
throw new UnderfundedError(candidates[0], null, this.address, {
|
|
214
|
+
line: `No offered payment row is affordable from this wallet (tried ${fundErrs.length}):\n`
|
|
215
|
+
+ fundErrs.map(({ sym, err }) => ` · ${sym}: ${err.message.replace(/^openzoo wallet underfunded: /, '')}`).join('\n'),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
192
218
|
onStage?.('paying');
|
|
193
219
|
const response = await fetch(url, {
|
|
194
220
|
...init,
|
package/lib/x402.js
CHANGED
|
@@ -74,8 +74,20 @@ export function evmChainId(network) {
|
|
|
74
74
|
* but RH stays opt-in for DEFAULT selection because its settlement asset has
|
|
75
75
|
* no auto-conversion path here.
|
|
76
76
|
*/
|
|
77
|
-
|
|
77
|
+
/**
|
|
78
|
+
* ALL payable rows from a 402, best-first. The wallet pays with whatever it
|
|
79
|
+
* HOLDS — the caller walks this list and takes the first affordable row, so
|
|
80
|
+
* a wallet rich in TOKEN but short of USDC pays the TOKEN row instead of
|
|
81
|
+
* erroring on the USDC one. Preference order within the list: the preferred
|
|
82
|
+
* symbol, then the rest of Solana (sponsored fees), then Base, then Robinhood
|
|
83
|
+
* (gated — paying there costs the wallet its own gas).
|
|
84
|
+
*/
|
|
85
|
+
export function orderAccepts(body, preferredSymbol, { allowRH = false, forceRail = null } = {}) {
|
|
78
86
|
const rows = parse402(body).accepts.filter((a) => a?.scheme === 'exact');
|
|
87
|
+
const bySym = (list) => [
|
|
88
|
+
...list.filter((a) => a?.extra?.symbol === preferredSymbol),
|
|
89
|
+
...list.filter((a) => a?.extra?.symbol !== preferredSymbol),
|
|
90
|
+
];
|
|
79
91
|
if (forceRail) {
|
|
80
92
|
const want = String(forceRail).toLowerCase();
|
|
81
93
|
if (!['solana', 'base', 'robinhood', 'evm'].includes(want)) {
|
|
@@ -88,19 +100,24 @@ export function pickAccept(body, preferredSymbol, { allowRH = false, forceRail =
|
|
|
88
100
|
`OPENZOO_RAIL=${want} but the live 402 offers no ${want} rail (offered: ${offered.join(', ') || 'none'})`,
|
|
89
101
|
);
|
|
90
102
|
}
|
|
91
|
-
return match
|
|
103
|
+
return bySym(match);
|
|
104
|
+
}
|
|
105
|
+
const out = [
|
|
106
|
+
...bySym(rows.filter((a) => railOf(a) === 'solana')),
|
|
107
|
+
...rows.filter((a) => railOf(a) === 'base'),
|
|
108
|
+
...rows.filter((a) => railOf(a) === 'evm'),
|
|
109
|
+
...(allowRH ? rows.filter((a) => railOf(a) === 'robinhood') : []),
|
|
110
|
+
];
|
|
111
|
+
if (!out.length) {
|
|
112
|
+
throw new Error(rows.some((a) => railOf(a) === 'robinhood')
|
|
113
|
+
? 'only Robinhood Chain rails offered — set OPENZOO_ENABLE_RH=1 or OPENZOO_RAIL=robinhood to use them (the rail settles; you must hold its settlement asset, see https://x402.accrue.fund/start)'
|
|
114
|
+
: 'no payable rail in 402 accepts[]');
|
|
92
115
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (evm.length) return evm[0];
|
|
99
|
-
const rh = rows.filter((a) => railOf(a) === 'robinhood');
|
|
100
|
-
if (allowRH && rh.length) return rh[0];
|
|
101
|
-
throw new Error(rh.length
|
|
102
|
-
? 'only Robinhood Chain rails offered — set OPENZOO_ENABLE_RH=1 or OPENZOO_RAIL=robinhood to use them (the rail settles; you must hold its settlement asset, see https://x402.accrue.fund/start)'
|
|
103
|
-
: 'no payable rail in 402 accepts[]');
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function pickAccept(body, preferredSymbol, opts = {}) {
|
|
120
|
+
return orderAccepts(body, preferredSymbol, opts)[0];
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
const mintCache = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|