openzoo 0.50.12 → 0.50.13
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/evm.js +30 -11
- package/lib/pay.js +16 -0
- package/lib/x402.js +15 -2
- package/lib/xbot.js +28 -4
- package/package.json +1 -1
package/lib/evm.js
CHANGED
|
@@ -63,6 +63,34 @@ export async function buildEvmPayment({ accept, evmPrivateKey, challenge }) {
|
|
|
63
63
|
nonce: `0x${crypto.randomBytes(32).toString('hex')}`,
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
// WHICH TYPED MESSAGE TO SIGN IS THE ROW'S DECISION, NOT OURS.
|
|
67
|
+
//
|
|
68
|
+
// Normal rows are EIP-3009 `TransferWithAuthorization` to the payee. A row
|
|
69
|
+
// marked `extra.settlement === "atomic"` settles through the gateway's
|
|
70
|
+
// AtomicSettle contract, which calls `receiveWithAuthorization` — and that
|
|
71
|
+
// requires msg.sender == to, so the payer must sign
|
|
72
|
+
// **ReceiveWithAuthorization** with `to` = the CONTRACT.
|
|
73
|
+
//
|
|
74
|
+
// The two payloads are IDENTICAL IN SHAPE, so signing the wrong one produces
|
|
75
|
+
// a payment that passes every local check and reverts on chain. The row tells
|
|
76
|
+
// us which it wants in `extra.eip3009`; we honour that and never guess.
|
|
77
|
+
//
|
|
78
|
+
// What the atomic row buys the payer: settlement happens AFTER the work, so
|
|
79
|
+
// the on-chain receipt binds the hash of the response they were served AND
|
|
80
|
+
// the upstream's own COGS transaction — a receipt for the delivery, not just
|
|
81
|
+
// a record of the debit.
|
|
82
|
+
const primaryType = accept.extra?.eip3009 === 'ReceiveWithAuthorization'
|
|
83
|
+
? 'ReceiveWithAuthorization'
|
|
84
|
+
: 'TransferWithAuthorization';
|
|
85
|
+
const authFields = [
|
|
86
|
+
{ name: 'from', type: 'address' },
|
|
87
|
+
{ name: 'to', type: 'address' },
|
|
88
|
+
{ name: 'value', type: 'uint256' },
|
|
89
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
90
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
91
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
92
|
+
];
|
|
93
|
+
|
|
66
94
|
const signature = await account.signTypedData({
|
|
67
95
|
domain: {
|
|
68
96
|
name: accept.extra?.name || 'USDC',
|
|
@@ -70,17 +98,8 @@ export async function buildEvmPayment({ accept, evmPrivateKey, challenge }) {
|
|
|
70
98
|
chainId,
|
|
71
99
|
verifyingContract: accept.asset,
|
|
72
100
|
},
|
|
73
|
-
types: {
|
|
74
|
-
|
|
75
|
-
{ name: 'from', type: 'address' },
|
|
76
|
-
{ name: 'to', type: 'address' },
|
|
77
|
-
{ name: 'value', type: 'uint256' },
|
|
78
|
-
{ name: 'validAfter', type: 'uint256' },
|
|
79
|
-
{ name: 'validBefore', type: 'uint256' },
|
|
80
|
-
{ name: 'nonce', type: 'bytes32' },
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
primaryType: 'TransferWithAuthorization',
|
|
101
|
+
types: { [primaryType]: authFields },
|
|
102
|
+
primaryType,
|
|
84
103
|
message: authorization,
|
|
85
104
|
});
|
|
86
105
|
|
package/lib/pay.js
CHANGED
|
@@ -268,6 +268,22 @@ export class PayClient {
|
|
|
268
268
|
// buy inference this wallet never paid for. The gateway is pay-per-request
|
|
269
269
|
// only, so any bearer the caller carried in is dropped here.
|
|
270
270
|
init = { ...init, headers: stripAuthorization(init.headers || {}) };
|
|
271
|
+
// ASK FOR THE RECEIPT-BEARING ROW.
|
|
272
|
+
//
|
|
273
|
+
// The gateway offers an AtomicSettle row on Base only to callers that say
|
|
274
|
+
// they understand it, because it changes WHICH typed message the payer must
|
|
275
|
+
// sign (ReceiveWithAuthorization, `to` = the contract) and a client that
|
|
276
|
+
// signed the standard one would produce a payment that reverts on chain.
|
|
277
|
+
// buildEvmPayment honours `extra.eip3009`, so this client does understand
|
|
278
|
+
// it — and what it gets in return is a settlement whose on-chain leaf binds
|
|
279
|
+
// the hash of the response it was served and the upstream's own COGS
|
|
280
|
+
// transaction, instead of a bare debit.
|
|
281
|
+
//
|
|
282
|
+
// Opt out with OPENZOO_ATOMIC=0 if a gateway ever offers the row and gets
|
|
283
|
+
// it wrong; the plain rows are always still there.
|
|
284
|
+
if (process.env.OPENZOO_ATOMIC !== '0') {
|
|
285
|
+
init = { ...init, headers: { ...(init.headers || {}), 'x-402-atomic': '1' } };
|
|
286
|
+
}
|
|
271
287
|
const first = await fetchHeaders(url, init);
|
|
272
288
|
if (first.status !== 402) {
|
|
273
289
|
return { response: first, paid: false };
|
package/lib/x402.js
CHANGED
|
@@ -178,9 +178,22 @@ export function orderAccepts(body, preferredSymbol, { allowRH = false, forceRail
|
|
|
178
178
|
const v = Number(a?.extra?.billedUsd);
|
|
179
179
|
return Number.isFinite(v) && v > 0 ? v : Infinity;
|
|
180
180
|
};
|
|
181
|
+
// AT THE SAME PRICE, TAKE THE ROW THAT COMES WITH A RECEIPT.
|
|
182
|
+
//
|
|
183
|
+
// The gateway's AtomicSettle row costs exactly what the plain Base row costs
|
|
184
|
+
// — it is the same quote, settled through a contract instead of the
|
|
185
|
+
// facilitator. What it adds is that the on-chain receipt binds the hash of
|
|
186
|
+
// the response we were served and the upstream's own COGS transaction, so
|
|
187
|
+
// "what we paid for" stops being something we have to take on trust.
|
|
188
|
+
//
|
|
189
|
+
// It is appended last in accepts[], so without this it never wins a tie and
|
|
190
|
+
// the plain row is always taken. Price still dominates: a cheaper row beats
|
|
191
|
+
// an atomic one, because a receipt is not worth paying extra for.
|
|
192
|
+
const atomic = (a) => (a?.extra?.settlement === 'atomic' ? 1 : 0);
|
|
193
|
+
const rank = (x, y) => priceOf(x) - priceOf(y) || atomic(y) - atomic(x);
|
|
181
194
|
const bySym = (list) => [
|
|
182
|
-
...list.filter((a) => a?.extra?.symbol === preferredSymbol),
|
|
183
|
-
...list.filter((a) => a?.extra?.symbol !== preferredSymbol).sort(
|
|
195
|
+
...list.filter((a) => a?.extra?.symbol === preferredSymbol).sort(rank),
|
|
196
|
+
...list.filter((a) => a?.extra?.symbol !== preferredSymbol).sort(rank),
|
|
184
197
|
];
|
|
185
198
|
if (forceRail) {
|
|
186
199
|
const want = String(forceRail).toLowerCase();
|
package/lib/xbot.js
CHANGED
|
@@ -717,6 +717,9 @@ const LOOSE_GATE = process.env.OPENZOO_XBOT_LOOSE_GATE !== '0';
|
|
|
717
717
|
|
|
718
718
|
/** Entries that mean work happened and must never repeat. Everything else in
|
|
719
719
|
* `answered` is a re-derivable judgement — see the note at the skip. */
|
|
720
|
+
/** How long to stay quiet after telling an asker their burner is empty. */
|
|
721
|
+
const PAYWALL_COOLDOWN_MS = Math.max(0, Number(process.env.OPENZOO_XBOT_PAYWALL_COOLDOWN_MS || 30 * 60_000));
|
|
722
|
+
|
|
720
723
|
const TERMINAL_VERDICTS = new Set(['answered', 'paid', 'paywalled', 'self', 'in_progress', 'failed']);
|
|
721
724
|
|
|
722
725
|
const SELF_TAG_RE = new RegExp(`@(?:${WATCH_HANDLES.map((h) => h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})`, 'gi');
|
|
@@ -2642,10 +2645,31 @@ export async function runXBot({ once = false, intervalMs = POLL_MS, dryRun = fal
|
|
|
2642
2645
|
saveState(state);
|
|
2643
2646
|
return;
|
|
2644
2647
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2648
|
+
// ONE FUNDING REQUEST, NOT ONE PER MENTION.
|
|
2649
|
+
//
|
|
2650
|
+
// OBSERVED 2026-08-26: six mentions from the same author between
|
|
2651
|
+
// 21:35 and 22:02 produced six IDENTICAL "your burner is out of
|
|
2652
|
+
// funds" replies, addresses and all. Nothing was duplicated — each
|
|
2653
|
+
// was a distinct tweet answered exactly once, and every dedupe guard
|
|
2654
|
+
// worked. The bug is that the answer to "you have no funds" does not
|
|
2655
|
+
// change when you ask again ninety seconds later, so repeating it is
|
|
2656
|
+
// pure noise in the asker's mentions and looks like a broken loop.
|
|
2657
|
+
//
|
|
2658
|
+
// The mention is still CLAIMED either way; the asker just does not
|
|
2659
|
+
// get told twice. Funding is what clears the cooldown — the balance
|
|
2660
|
+
// check on the next question is the real reset, this timer only
|
|
2661
|
+
// stops the shouting in between.
|
|
2662
|
+
const lastPw = Number((state.paywallAt || {})[t.author_id] || 0);
|
|
2663
|
+
const quiet = Date.now() - lastPw < PAYWALL_COOLDOWN_MS;
|
|
2664
|
+
if (quiet) {
|
|
2665
|
+
console.error(` ${t.id} @${t.author_id}: PAYWALL suppressed (told ${Math.round((Date.now() - lastPw) / 1000)}s ago)`);
|
|
2666
|
+
} else {
|
|
2667
|
+
const text = composePaywallReply(t.author_id, t.id, { address: burner.address, returning: Boolean(state.freeUsed[t.author_id]), quotedUsd: quotedUsdFrom(e.message) });
|
|
2668
|
+
console.error(` ${t.id} @${t.author_id}: PAYWALL → burner ${burner.address}`);
|
|
2669
|
+
await postAndLog({ creds, text, inReplyTo: t.id, state, dryRun, tag: 'paywall', conversationId: t.conversation_id })
|
|
2670
|
+
.catch((err) => console.error(` reply failed: ${err.message}`));
|
|
2671
|
+
state.paywallAt = { ...(state.paywallAt || {}), [t.author_id]: Date.now() };
|
|
2672
|
+
}
|
|
2649
2673
|
state.answered[t.id] = 'paywalled';
|
|
2650
2674
|
}
|
|
2651
2675
|
saveState(state);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.13",
|
|
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",
|