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 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
- TransferWithAuthorization: [
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((x, y) => priceOf(x) - priceOf(y)),
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
- const text = composePaywallReply(t.author_id, t.id, { address: burner.address, returning: Boolean(state.freeUsed[t.author_id]), quotedUsd: quotedUsdFrom(e.message) });
2646
- console.error(` ${t.id} @${t.author_id}: PAYWALL → burner ${burner.address}`);
2647
- await postAndLog({ creds, text, inReplyTo: t.id, state, dryRun, tag: 'paywall', conversationId: t.conversation_id })
2648
- .catch((err) => console.error(` reply failed: ${err.message}`));
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.12",
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",