@zeam-labs/x402-mcp-bridge 2.0.4 → 2.0.6

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.
Files changed (3) hide show
  1. package/README.md +9 -12
  2. package/index.mjs +28 -28
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,9 +62,8 @@ settlement contract, withdrawable by your side of the channel alone: the
62
62
  for most clients is the same key but need not be.
63
63
 
64
64
  That escrow is not the seller's. It is x402's own batch-settlement contract,
65
- hardcoded in [`@x402/evm`](https://www.npmjs.com/package/@x402/evm) and published
66
- to npm by Coinbase in `2.12.0` on 2026-05-13 two days before the contract
67
- existed on Base. No owner, no pause, no upgrade, no sweep.
65
+ hardcoded in [`@x402/evm`](https://www.npmjs.com/package/@x402/evm), published by
66
+ Coinbase. No owner, no pause, no upgrade, no sweep.
68
67
 
69
68
  ```
70
69
  npm pack @x402/evm@2.12.0 && grep -rl 0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003 package/
@@ -92,9 +91,8 @@ probes are also unpaid calls, and a server may cap how many of those it will
92
91
  answer, which is how a funded wallet gets locked out of a channel it has money in.
93
92
 
94
93
  The terms are static and published, so this bridge reads them once from
95
- `/.well-known/x402` at connect and attaches payment to its **first** request.
96
- Measured against the same server: six calls issued six 402 challenges the old
97
- way, and zero the new way.
94
+ `/.well-known/x402` at connect and attaches payment to its **first** request
95
+ no unpaid probe, no extra round trip.
98
96
 
99
97
  A refused payment re-reads the terms and retries once, because quotes for
100
98
  non-stable assets move with the oracle. Anything still failing falls back to the
@@ -132,8 +130,7 @@ if that fails too. That ordering matters: falling straight through to per-call
132
130
  payment turns one closed line into a signed payment per in-flight call,
133
131
  serialized behind one channel, and when those run out of road they become unpaid
134
132
  requests that burn the hourly ceiling and lock a funded wallet out of its own
135
- channel. Measured before the fix, at 25-way concurrency: 3 of 12 calls served.
136
- After: 200 of 200 across 8 line deaths.
133
+ channel.
137
134
 
138
135
  **Cold starts.** A voucher signs a *cumulative* total, and that total is not on
139
136
  the chain — the escrow knows your balance and what has been claimed, not what has
@@ -161,7 +158,7 @@ time and an overlapping tick is refused as `channel_busy`.
161
158
  | `X402_STATE_DIR` | `~/.x402-mcp-bridge/<host>/<address>` | channel state |
162
159
  | `X402_SALT` | scheme default | open a distinct channel. Any string; it is hashed to bytes32 |
163
160
  | `X402_MAX_SPEND` | `10000000` (=$10) | ceiling on what **this run** may spend, in micro-USD. `0` removes it — see below |
164
- | `X402_DEPOSIT_MULTIPLIER` | `400` | how much collateral a deposit puts in escrow, as a multiple of the quote. **The quote is 250 micro-USD, so the default deposit is 400 x 250 = 100,000 micro-USD = $0.10.** That is refundable collateral, not a charge but it leaves your wallet the moment you open a channel, and no page said the number out loud until a cold buyer had to multiply two figures from two documents to find out what plugging in the config would cost it. Lower it if $0.10 is more than you want committed; the scheme refuses below 3x. |
161
+ | `X402_DEPOSIT_MULTIPLIER` | *scheme default* | how much **refundable** collateral to lock, as a multiple of the seller's quote for the opening call. Unset, the x402 scheme sizes it (minimum 3); raise it to top up less often, lower it to commit less. It leaves your wallet when you open the channel and comes back on refund it is not the price. |
165
162
 
166
163
  ## It stops spending when you stop watching
167
164
 
@@ -185,9 +182,9 @@ signer can **read the chain** — so this bridge always gives the signer a reade
185
182
  By default that reader is the upstream's own free `/verify` surface, which means
186
183
  recovery costs nothing and needs no RPC of your own.
187
184
 
188
- Measured against `mcp.zeamprism.com`: fresh channel, first call 2.6s (one
189
- on-chain deposit) then ~180ms per call. State deliberately wiped: healed and
190
- served in 3.9s, then ~150ms.
185
+ The first call also makes the on-chain deposit, so it is slower than the rest;
186
+ every call after it is fast. Losing state adds one more deposit-time call, then
187
+ it is fast again.
191
188
 
192
189
  ## What it does not do
193
190
 
package/index.mjs CHANGED
@@ -42,8 +42,9 @@ if (has('--help') || has('-h')) {
42
42
  '',
43
43
  'Env: X402_PRIVATE_KEY (required), X402_MCP_URL (X402_UPSTREAM also accepted),',
44
44
  ' X402_MAX_SPEND (0 = no cap; base units of the paid asset if the server publishes no price),',
45
- ' X402_DEPOSIT_MULTIPLIER (default 400 × the SELLER\'S quoted per-call amount; against',
46
- ' zeamprism that is 400 × 250 = $0.10 of refundable collateral), X402_LINE=auto|on|off,',
45
+ ' X402_DEPOSIT_MULTIPLIER (refundable collateral to lock, as a multiple of',
46
+ ' the opening quote; unset uses the x402 scheme default, minimum 3),',
47
+ ' X402_LINE=auto|on|off,',
47
48
  ' X402_SALT.',
48
49
  ' auto: buy per-call minimum holds until calls arrive faster than the',
49
50
  ' server minimum hold, then hold a line while that lasts. A held line',
@@ -99,9 +100,11 @@ const watchedStorage = {
99
100
  try {
100
101
  const f = readdirSync(join(stateDir, 'client')).find((n) => n.endsWith('.json'))
101
102
  if (f) channelId = f.replace(/\.json$/, '')
102
- } catch { /* first run, nothing to recover */ }
103
+ } catch {}
103
104
 
104
- const depositPolicy = { depositMultiplier: Number(process.env.X402_DEPOSIT_MULTIPLIER ?? 400) }
105
+ const depositPolicy = process.env.X402_DEPOSIT_MULTIPLIER
106
+ ? { depositMultiplier: Number(process.env.X402_DEPOSIT_MULTIPLIER) }
107
+ : {}
105
108
 
106
109
  const MAX_SPEND = Number(process.env.X402_MAX_SPEND ?? 10_000_000)
107
110
  let capReached = false
@@ -109,12 +112,12 @@ let capReached = false
109
112
  let startedAt = null
110
113
  let spentMicroUSD = 0
111
114
 
112
- let quoteMicroUSD = null // set from the seller's own terms
113
- let spendUnit = 'micro-USD' // what the numbers we report actually ARE
115
+ let quoteMicroUSD = null
116
+ let spendUnit = 'micro-USD'
114
117
 
115
118
  const quoteFromTerms = (j) => {
116
119
  for (const c of [j?.rate?.deposit?.tickQuoteMicroUSD, j?.rate?.tickQuoteMicroUSD,
117
- j?.rate?.microUSDPerCall, j?.quoteMicroUSD]) {
120
+ j?.rate?.microUSDPerCall, j?.rate?.microUSDPerBlock, j?.quoteMicroUSD]) {
118
121
  const n = Number(c)
119
122
  if (Number.isFinite(n) && n > 0) return n
120
123
  }
@@ -134,7 +137,7 @@ const noteBilled = (ctx) => {
134
137
  if (startedAt === null) startedAt = charged
135
138
  const spent = microUSDOf(charged - startedAt)
136
139
  if (spent > spentMicroUSD) spentMicroUSD = spent
137
- } catch { /* a record we cannot read is not a reason to stop paying */ }
140
+ } catch {}
138
141
  if (!MAX_SPEND || capReached || spentMicroUSD < MAX_SPEND) return
139
142
  capReached = true
140
143
  log(`SPEND CAP REACHED — this run has spent ${spentMicroUSD} ${spendUnit} against a cap of ` +
@@ -156,15 +159,14 @@ const cardSigner = CARD_PAYER ? {
156
159
  readContract: pub.readContract.bind(pub),
157
160
  signTypedData: () => {
158
161
  throw new Error(
159
- 'this is a gift card and it cannot add funds: it holds a spending key, not ' +
160
- `the key to ${CARD_PAYER}. The card is out of money ask whoever funded it ` +
161
- 'to top it up (agent-wallet fund) or issue a new one.')
162
+ `this key spends ${CARD_PAYER}'s channel but cannot add funds to it it is a ` +
163
+ 'spending key, not that wallet\'s key. Top up from the wallet that owns the channel.')
162
164
  },
163
165
  } : null
164
166
 
165
167
  if (CARD_PAYER) {
166
- log(`gift card mode — spending ${CARD_PAYER}'s channel, authorized as ${account.address}`)
167
- log('this key can spend the card and send it home; it cannot move the money anywhere else')
168
+ log(`spending ${CARD_PAYER}'s channel, authorized as ${account.address}`)
169
+ log('this key can spend that channel and return it; it cannot move the money elsewhere')
168
170
  }
169
171
 
170
172
  const payments = new x402Client(selector).register(NETWORK,
@@ -191,7 +193,7 @@ const loadTerms = async () => {
191
193
  accepts = { x402Version: j.x402Version ?? 1, accepts: j.accepts }
192
194
  tickAccepts = Array.isArray(j.tickAccepts) && j.tickAccepts.length
193
195
  ? { x402Version: j.x402Version ?? 1, accepts: j.tickAccepts }
194
- : accepts // an upstream that does not sell time
196
+ : accepts
195
197
  const ln = j.limits?.line ?? j.payment?.limits?.line
196
198
  lineFacts = {
197
199
  tickMs: Number(ln?.tickMs) > 0 ? Number(ln.tickMs) : lineFacts.tickMs,
@@ -206,8 +208,7 @@ const loadTerms = async () => {
206
208
  } else if (quoteMicroUSD === null) {
207
209
  spendUnit = 'base units of the paid asset'
208
210
  log(`quote: this server publishes no micro-USD price, so X402_MAX_SPEND is read as ` +
209
- `BASE UNITS OF THE ASSET, not dollars. Guessing a price here is how a spend cap ` +
210
- `silently stops capping.`)
211
+ `base units of the asset, not dollars.`)
211
212
  }
212
213
  return accepts
213
214
  }
@@ -217,7 +218,7 @@ catch (e) { log(`could not cache terms (${e.message}); falling back to probe-the
217
218
  let paymentQueue = Promise.resolve()
218
219
  const oneAtATime = (fn) => {
219
220
  const run = paymentQueue.then(fn, fn)
220
- paymentQueue = run.then(() => {}, () => {}) // a failure must not poison the queue
221
+ paymentQueue = run.then(() => {}, () => {})
221
222
  return run
222
223
  }
223
224
 
@@ -244,13 +245,13 @@ const payNow = async (name, args) => {
244
245
  if (explainPermit2(out)) return out
245
246
  if (refusedPayment(out)) {
246
247
  log('payment refused as stale — dropping the local channel record and resyncing')
247
- if (channelId) { try { await watchedStorage.delete(channelId) } catch { /* it will be rebuilt */ } }
248
+ if (channelId) { try { await watchedStorage.delete(channelId) } catch {} }
248
249
  return upstream.callTool(name, args)
249
250
  }
250
251
  return out
251
252
  } catch (e) {
252
253
  if (attempt === 2) { log(`pay-first failed twice (${e.message}); using probe path`); return upstream.callTool(name, args) }
253
- try { await loadTerms() } catch { /* keep the old terms and let attempt 2 decide */ }
254
+ try { await loadTerms() } catch {}
254
255
  }
255
256
  }
256
257
  }
@@ -264,7 +265,7 @@ const explainPermit2 = (out) => {
264
265
  if (!token || approvalToldFor === token) return true
265
266
  approvalToldFor = token
266
267
  const per = Number(chosenAccept?.amount ?? 0)
267
- const mult = Number(process.env.X402_DEPOSIT_MULTIPLIER ?? 400)
268
+ const mult = Number(process.env.X402_DEPOSIT_MULTIPLIER ?? 5)
268
269
  const suggested = per > 0 ? BigInt(Math.ceil(per * mult * 4)) : 0n
269
270
  log(`${token} moves through Permit2 and your wallet has not approved it.`)
270
271
  log(` send once, from your wallet: approve(${PERMIT2}, ${suggested || '<amount>'}) on ${token}`)
@@ -311,7 +312,7 @@ const wsURL = () => {
311
312
 
312
313
  const dropLine = (why) => {
313
314
  if (line.timer) { clearInterval(line.timer); line.timer = null }
314
- try { line.socket?.close() } catch { /* already gone */ }
315
+ try { line.socket?.close() } catch {}
315
316
  if (line.credential) log(`line closed (${why})`)
316
317
  line.socket = null
317
318
  line.credential = null
@@ -334,7 +335,7 @@ const tick = async () => {
334
335
 
335
336
  const openLine = () => {
336
337
  if (line.credential || line.opening) return line.opening
337
- if (!channelId) return null // no channel yet; the first paid call makes one
338
+ if (!channelId) return null
338
339
  line.opening = new Promise((resolve) => {
339
340
  let socket
340
341
  try { socket = new WebSocket(wsURL()) } catch (e) { log(`line: ${e.message}`); return resolve(null) }
@@ -379,7 +380,7 @@ const openLine = () => {
379
380
  }
380
381
  socket.onopen = () => socket.send(JSON.stringify({ op: 'open', channelId }))
381
382
  socket.onclose = () => { clearTimeout(give_up); dropLine('socket closed'); resolve(null) }
382
- socket.onerror = () => { /* onclose follows and does the work */ }
383
+ socket.onerror = () => {}
383
384
  }).finally(() => { line.opening = null })
384
385
  return line.opening
385
386
  }
@@ -421,7 +422,7 @@ const callOnLine = async (name, args) => {
421
422
  dropLine(reasonFrom(out) ?? 'refused by the server')
422
423
  if (attempt === 2) {
423
424
  log('the line was refused twice; paying for this call directly. If the reason above ' +
424
- 'is collateral, raise X402_DEPOSIT_MULTIPLIER — the minimum of 3 buys under a second of line.')
425
+ 'is collateral, raise X402_DEPOSIT_MULTIPLIER — the scheme minimum is 3.')
425
426
  }
426
427
  }
427
428
  return payFirst(name, args)
@@ -519,10 +520,9 @@ if (has('--refund')) {
519
520
  'The exit that always works needs nothing from us:',
520
521
  ' initiateWithdraw(config, amount) then, after the delay, finalizeWithdraw(config)',
521
522
  'We also watch for that first call and return the collateral ourselves, at our gas,',
522
- 'so you usually do not have to send the second transaction. We do NOT promise when:',
523
- 'a third-party auditor measured 920 seconds on 2026-08-26, i.e. just after the delay',
524
- 'elapsed. Plan against the delay. The escrow gates withdrawal to you alone, so your',
525
- 'unspent collateral is safe either way.',
523
+ 'so you usually do not have to send the second transaction but not on a promised',
524
+ 'schedule; it can lag until just past the delay window. Plan against the delay. The',
525
+ 'escrow gates withdrawal to you alone, so your unspent collateral is safe either way.',
526
526
  '',
527
527
  ].join('\n'))
528
528
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeam-labs/x402-mcp-bridge",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Put a wallet in front of any x402-paid MCP server, and hold a metered line on the ones that sell time. Stock MCP clients cannot construct x402 payments; this one does, and proxies your existing client through it.",
5
5
  "type": "module",
6
6
  "bin": {