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

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 +24 -25
  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` | `400` | collateral a deposit locks in escrow, as a multiple of the seller's quote for the opening call. That opening quote carries the one-time open fee, so against zeamprism the default locks ~$0.70 of **refundable** collateral — it leaves your wallet when you open the channel and returns on refund. Lower it if that is more than you want committed; the scheme refuses below 3x. |
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,10 @@ 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 (default 400; the deposit is this times the seller\'s',
46
+ ' quote for the OPENING call, held as refundable collateral — against',
47
+ ' zeamprism ~$0.70, since the opening quote carries the one-time open',
48
+ ' fee), X402_LINE=auto|on|off,',
47
49
  ' X402_SALT.',
48
50
  ' auto: buy per-call minimum holds until calls arrive faster than the',
49
51
  ' server minimum hold, then hold a line while that lasts. A held line',
@@ -99,7 +101,7 @@ const watchedStorage = {
99
101
  try {
100
102
  const f = readdirSync(join(stateDir, 'client')).find((n) => n.endsWith('.json'))
101
103
  if (f) channelId = f.replace(/\.json$/, '')
102
- } catch { /* first run, nothing to recover */ }
104
+ } catch {}
103
105
 
104
106
  const depositPolicy = { depositMultiplier: Number(process.env.X402_DEPOSIT_MULTIPLIER ?? 400) }
105
107
 
@@ -109,12 +111,12 @@ let capReached = false
109
111
  let startedAt = null
110
112
  let spentMicroUSD = 0
111
113
 
112
- let quoteMicroUSD = null // set from the seller's own terms
113
- let spendUnit = 'micro-USD' // what the numbers we report actually ARE
114
+ let quoteMicroUSD = null
115
+ let spendUnit = 'micro-USD'
114
116
 
115
117
  const quoteFromTerms = (j) => {
116
118
  for (const c of [j?.rate?.deposit?.tickQuoteMicroUSD, j?.rate?.tickQuoteMicroUSD,
117
- j?.rate?.microUSDPerCall, j?.quoteMicroUSD]) {
119
+ j?.rate?.microUSDPerCall, j?.rate?.microUSDPerBlock, j?.quoteMicroUSD]) {
118
120
  const n = Number(c)
119
121
  if (Number.isFinite(n) && n > 0) return n
120
122
  }
@@ -134,7 +136,7 @@ const noteBilled = (ctx) => {
134
136
  if (startedAt === null) startedAt = charged
135
137
  const spent = microUSDOf(charged - startedAt)
136
138
  if (spent > spentMicroUSD) spentMicroUSD = spent
137
- } catch { /* a record we cannot read is not a reason to stop paying */ }
139
+ } catch {}
138
140
  if (!MAX_SPEND || capReached || spentMicroUSD < MAX_SPEND) return
139
141
  capReached = true
140
142
  log(`SPEND CAP REACHED — this run has spent ${spentMicroUSD} ${spendUnit} against a cap of ` +
@@ -156,15 +158,14 @@ const cardSigner = CARD_PAYER ? {
156
158
  readContract: pub.readContract.bind(pub),
157
159
  signTypedData: () => {
158
160
  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.')
161
+ `this key spends ${CARD_PAYER}'s channel but cannot add funds to it it is a ` +
162
+ 'spending key, not that wallet\'s key. Top up from the wallet that owns the channel.')
162
163
  },
163
164
  } : null
164
165
 
165
166
  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')
167
+ log(`spending ${CARD_PAYER}'s channel, authorized as ${account.address}`)
168
+ log('this key can spend that channel and return it; it cannot move the money elsewhere')
168
169
  }
169
170
 
170
171
  const payments = new x402Client(selector).register(NETWORK,
@@ -191,7 +192,7 @@ const loadTerms = async () => {
191
192
  accepts = { x402Version: j.x402Version ?? 1, accepts: j.accepts }
192
193
  tickAccepts = Array.isArray(j.tickAccepts) && j.tickAccepts.length
193
194
  ? { x402Version: j.x402Version ?? 1, accepts: j.tickAccepts }
194
- : accepts // an upstream that does not sell time
195
+ : accepts
195
196
  const ln = j.limits?.line ?? j.payment?.limits?.line
196
197
  lineFacts = {
197
198
  tickMs: Number(ln?.tickMs) > 0 ? Number(ln.tickMs) : lineFacts.tickMs,
@@ -206,8 +207,7 @@ const loadTerms = async () => {
206
207
  } else if (quoteMicroUSD === null) {
207
208
  spendUnit = 'base units of the paid asset'
208
209
  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.`)
210
+ `base units of the asset, not dollars.`)
211
211
  }
212
212
  return accepts
213
213
  }
@@ -217,7 +217,7 @@ catch (e) { log(`could not cache terms (${e.message}); falling back to probe-the
217
217
  let paymentQueue = Promise.resolve()
218
218
  const oneAtATime = (fn) => {
219
219
  const run = paymentQueue.then(fn, fn)
220
- paymentQueue = run.then(() => {}, () => {}) // a failure must not poison the queue
220
+ paymentQueue = run.then(() => {}, () => {})
221
221
  return run
222
222
  }
223
223
 
@@ -244,13 +244,13 @@ const payNow = async (name, args) => {
244
244
  if (explainPermit2(out)) return out
245
245
  if (refusedPayment(out)) {
246
246
  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 */ } }
247
+ if (channelId) { try { await watchedStorage.delete(channelId) } catch {} }
248
248
  return upstream.callTool(name, args)
249
249
  }
250
250
  return out
251
251
  } catch (e) {
252
252
  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 */ }
253
+ try { await loadTerms() } catch {}
254
254
  }
255
255
  }
256
256
  }
@@ -311,7 +311,7 @@ const wsURL = () => {
311
311
 
312
312
  const dropLine = (why) => {
313
313
  if (line.timer) { clearInterval(line.timer); line.timer = null }
314
- try { line.socket?.close() } catch { /* already gone */ }
314
+ try { line.socket?.close() } catch {}
315
315
  if (line.credential) log(`line closed (${why})`)
316
316
  line.socket = null
317
317
  line.credential = null
@@ -334,7 +334,7 @@ const tick = async () => {
334
334
 
335
335
  const openLine = () => {
336
336
  if (line.credential || line.opening) return line.opening
337
- if (!channelId) return null // no channel yet; the first paid call makes one
337
+ if (!channelId) return null
338
338
  line.opening = new Promise((resolve) => {
339
339
  let socket
340
340
  try { socket = new WebSocket(wsURL()) } catch (e) { log(`line: ${e.message}`); return resolve(null) }
@@ -379,7 +379,7 @@ const openLine = () => {
379
379
  }
380
380
  socket.onopen = () => socket.send(JSON.stringify({ op: 'open', channelId }))
381
381
  socket.onclose = () => { clearTimeout(give_up); dropLine('socket closed'); resolve(null) }
382
- socket.onerror = () => { /* onclose follows and does the work */ }
382
+ socket.onerror = () => {}
383
383
  }).finally(() => { line.opening = null })
384
384
  return line.opening
385
385
  }
@@ -519,10 +519,9 @@ if (has('--refund')) {
519
519
  'The exit that always works needs nothing from us:',
520
520
  ' initiateWithdraw(config, amount) then, after the delay, finalizeWithdraw(config)',
521
521
  '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.',
522
+ 'so you usually do not have to send the second transaction but not on a promised',
523
+ 'schedule; it can lag until just past the delay window. Plan against the delay. The',
524
+ 'escrow gates withdrawal to you alone, so your unspent collateral is safe either way.',
526
525
  '',
527
526
  ].join('\n'))
528
527
  }
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.5",
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": {