@zeam-labs/x402-mcp-bridge 2.0.3 → 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 +26 -32
  2. package/index.mjs +39 -62
  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/
@@ -87,16 +86,13 @@ accept EIP-3009, which needs no approval at all.
87
86
 
88
87
  The stock x402 flow sends every call **unpaid**, reads the 402 it comes back
89
88
  with, and then sends the same call again carrying payment. Two network round
90
- trips for one call. At a 130ms round trip that is 260ms instead of 130ms — and on
91
- a meter that bills time on the line, the buyer pays for a handshake nobody
92
- needed. Those probes are also unpaid calls, and a server may cap how many of
93
- those it will answer, which is how a funded wallet gets locked out of a channel
94
- it has money in.
89
+ trips for one call. At a 130ms round trip that is 260ms instead of 130ms. Those
90
+ probes are also unpaid calls, and a server may cap how many of those it will
91
+ answer, which is how a funded wallet gets locked out of a channel it has money in.
95
92
 
96
93
  The terms are static and published, so this bridge reads them once from
97
- `/.well-known/x402` at connect and attaches payment to its **first** request.
98
- Measured against the same server: six calls issued six 402 challenges the old
99
- 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.
100
96
 
101
97
  A refused payment re-reads the terms and retries once, because quotes for
102
98
  non-stable assets move with the oracle. Anything still failing falls back to the
@@ -104,21 +100,21 @@ old probe-then-pay path rather than dropping your call.
104
100
 
105
101
  ## Holding a line
106
102
 
107
- Upstream sells **time**, not calls, and there is a cheaper way to buy it than
108
- paying per call.
103
+ A server may sell **time** rather than calls, with a cheaper path than paying
104
+ per call. The pricing is the server's — read it in its published terms — and this
105
+ bridge drives it for you:
109
106
 
110
107
  1. Deposit once — your first paid call does it for you.
111
- 2. Open a line on the endpoint's `/pay` websocket. It hands back a credential.
108
+ 2. Open a line on the endpoint's `/pay` websocket. If the server challenges,
109
+ the bridge signs the challenge with your key to prove the channel is yours,
110
+ and gets back a credential.
112
111
  3. Call the `tick` tool on a steady cadence, passing `{line: "<credential>"}`.
113
- That is an ordinary paid call and it buys the milliseconds since your previous
114
- tick.
115
- 4. Every other call carries only `{line: "<credential>"}` and costs **nothing** —
116
- no signature per call, nothing to serialize, and as many calls in flight at
117
- once as you like.
112
+ That is an ordinary paid call and it pays the server for more time.
113
+ 4. Every other call carries only `{line: "<credential>"}` and no payment, and as
114
+ many can be in flight at once as you like.
118
115
 
119
- Stop ticking and the line closes. An open line bills the whole time it is open,
120
- so a line you forget costs at most one idle-close window past your last tick and
121
- then stops existing.
116
+ Stop ticking and the line lapses. What the server charges, when a line lapses,
117
+ and whether unused time is kept are the server's to state, not this bridge's.
122
118
 
123
119
  **This bridge drives a line for you.** `X402_LINE` controls it:
124
120
 
@@ -134,8 +130,7 @@ if that fails too. That ordering matters: falling straight through to per-call
134
130
  payment turns one closed line into a signed payment per in-flight call,
135
131
  serialized behind one channel, and when those run out of road they become unpaid
136
132
  requests that burn the hourly ceiling and lock a funded wallet out of its own
137
- channel. Measured before the fix, at 25-way concurrency: 3 of 12 calls served.
138
- After: 200 of 200 across 8 line deaths.
133
+ channel.
139
134
 
140
135
  **Cold starts.** A voucher signs a *cumulative* total, and that total is not on
141
136
  the chain — the escrow knows your balance and what has been claimed, not what has
@@ -146,10 +141,9 @@ it happens once; lose it, or run the same key on a second machine, and it happen
146
141
  again on the next call and then not after.
147
142
 
148
143
  Two things worth knowing if you write your own client. Pay a tick against
149
- `tickAccepts` from `/.well-known/x402` same figure as a call, and the ceiling on
150
- one tick. And never send two ticks at once: a voucher signs a cumulative total, so
151
- a channel carries one payment at a time and an overlapping tick is refused as
152
- `channel_busy`.
144
+ `tickAccepts` from `/.well-known/x402`, not `accepts`. And never send two ticks at
145
+ once: a voucher signs a cumulative total, so a channel carries one payment at a
146
+ time and an overlapping tick is refused as `channel_busy`.
153
147
 
154
148
  ## Configuration
155
149
 
@@ -164,7 +158,7 @@ a channel carries one payment at a time and an overlapping tick is refused as
164
158
  | `X402_STATE_DIR` | `~/.x402-mcp-bridge/<host>/<address>` | channel state |
165
159
  | `X402_SALT` | scheme default | open a distinct channel. Any string; it is hashed to bytes32 |
166
160
  | `X402_MAX_SPEND` | `10000000` (=$10) | ceiling on what **this run** may spend, in micro-USD. `0` removes it — see below |
167
- | `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. |
168
162
 
169
163
  ## It stops spending when you stop watching
170
164
 
@@ -188,9 +182,9 @@ signer can **read the chain** — so this bridge always gives the signer a reade
188
182
  By default that reader is the upstream's own free `/verify` surface, which means
189
183
  recovery costs nothing and needs no RPC of your own.
190
184
 
191
- Measured against `mcp.zeamprism.com`: fresh channel, first call 2.6s (one
192
- on-chain deposit) then ~180ms per call. State deliberately wiped: healed and
193
- 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.
194
188
 
195
189
  ## What it does not do
196
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',
@@ -54,7 +56,6 @@ if (has('--help') || has('-h')) {
54
56
  process.exit(0)
55
57
  }
56
58
 
57
-
58
59
  if (!KEY || !/^0x[0-9a-fA-F]{64}$/.test(KEY)) {
59
60
  log('set X402_PRIVATE_KEY to a 0x-prefixed 32-byte key. It stays on this machine;')
60
61
  log('it signs payment vouchers locally and is never sent anywhere.')
@@ -69,8 +70,6 @@ const stateDir = process.env.X402_STATE_DIR ??
69
70
  join(homedir(), '.x402-mcp-bridge', new URL(UPSTREAM).host, account.address.toLowerCase())
70
71
  mkdirSync(stateDir, { recursive: true })
71
72
 
72
- // The seller's free scoped RPC, last. It reaches their node without a wallet,
73
- // which is what makes it usable before you have paid them anything.
74
73
  const readers = [
75
74
  ...(process.env.X402_RPC_URL ? [process.env.X402_RPC_URL] : []),
76
75
  ...(chain.rpcUrls?.default?.http ?? []),
@@ -97,15 +96,12 @@ let channelId = null
97
96
  const watchedStorage = {
98
97
  get: (k) => storage.get(k),
99
98
  delete: (k) => storage.delete(k),
100
- // Every write carries the seller's running total for this channel, so this is
101
- // also where we learn what we have been billed -- see noteBilled below.
102
99
  set: (k, ctx) => { channelId = k; noteBilled(ctx); return storage.set(k, ctx) },
103
100
  }
104
- // And across restarts: the scheme persists one file per channel.
105
101
  try {
106
102
  const f = readdirSync(join(stateDir, 'client')).find((n) => n.endsWith('.json'))
107
103
  if (f) channelId = f.replace(/\.json$/, '')
108
- } catch { /* first run, nothing to recover */ }
104
+ } catch {}
109
105
 
110
106
  const depositPolicy = { depositMultiplier: Number(process.env.X402_DEPOSIT_MULTIPLIER ?? 400) }
111
107
 
@@ -115,14 +111,12 @@ let capReached = false
115
111
  let startedAt = null
116
112
  let spentMicroUSD = 0
117
113
 
118
- let quoteMicroUSD = null // set from the seller's own terms
119
- let spendUnit = 'micro-USD' // what the numbers we report actually ARE
114
+ let quoteMicroUSD = null
115
+ let spendUnit = 'micro-USD'
120
116
 
121
- // Liberal in where it looks, strict about giving up. A server that does not say
122
- // what a call costs in USD gets NO GUESS.
123
117
  const quoteFromTerms = (j) => {
124
118
  for (const c of [j?.rate?.deposit?.tickQuoteMicroUSD, j?.rate?.tickQuoteMicroUSD,
125
- j?.rate?.microUSDPerCall, j?.quoteMicroUSD]) {
119
+ j?.rate?.microUSDPerCall, j?.rate?.microUSDPerBlock, j?.quoteMicroUSD]) {
126
120
  const n = Number(c)
127
121
  if (Number.isFinite(n) && n > 0) return n
128
122
  }
@@ -130,8 +124,6 @@ const quoteFromTerms = (j) => {
130
124
  }
131
125
 
132
126
  const microUSDOf = (units) => {
133
- // The entry the selector actually chose. Not accepts[0] -- that is USDC, and
134
- // converting WETH units by a USDC quote is the same units bug in a new hat.
135
127
  const quoted = Number(chosenAccept?.amount ?? 0)
136
128
  if (quoteMicroUSD === null || !(quoted > 0)) return units
137
129
  return units * (quoteMicroUSD / quoted)
@@ -144,7 +136,7 @@ const noteBilled = (ctx) => {
144
136
  if (startedAt === null) startedAt = charged
145
137
  const spent = microUSDOf(charged - startedAt)
146
138
  if (spent > spentMicroUSD) spentMicroUSD = spent
147
- } catch { /* a record we cannot read is not a reason to stop paying */ }
139
+ } catch {}
148
140
  if (!MAX_SPEND || capReached || spentMicroUSD < MAX_SPEND) return
149
141
  capReached = true
150
142
  log(`SPEND CAP REACHED — this run has spent ${spentMicroUSD} ${spendUnit} against a cap of ` +
@@ -166,15 +158,14 @@ const cardSigner = CARD_PAYER ? {
166
158
  readContract: pub.readContract.bind(pub),
167
159
  signTypedData: () => {
168
160
  throw new Error(
169
- 'this is a gift card and it cannot add funds: it holds a spending key, not ' +
170
- `the key to ${CARD_PAYER}. The card is out of money ask whoever funded it ` +
171
- '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.')
172
163
  },
173
164
  } : null
174
165
 
175
166
  if (CARD_PAYER) {
176
- log(`gift card mode — spending ${CARD_PAYER}'s channel, authorized as ${account.address}`)
177
- 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')
178
169
  }
179
170
 
180
171
  const payments = new x402Client(selector).register(NETWORK,
@@ -193,8 +184,6 @@ log(`paying as ${account.address} -> ${UPSTREAM}`)
193
184
  const termsURL = new URL('/.well-known/x402', UPSTREAM).toString()
194
185
  let accepts = null
195
186
  let tickAccepts = null
196
- // The seller's line facts: how often to tick, and what a millisecond costs.
197
- // They belong to the service, not to one line, so they come from the manifest.
198
187
  let lineFacts = { tickMs: 250, microUSDPerMs: null }
199
188
  const loadTerms = async () => {
200
189
  const r = await fetch(termsURL)
@@ -203,7 +192,7 @@ const loadTerms = async () => {
203
192
  accepts = { x402Version: j.x402Version ?? 1, accepts: j.accepts }
204
193
  tickAccepts = Array.isArray(j.tickAccepts) && j.tickAccepts.length
205
194
  ? { x402Version: j.x402Version ?? 1, accepts: j.tickAccepts }
206
- : accepts // an upstream that does not sell time
195
+ : accepts
207
196
  const ln = j.limits?.line ?? j.payment?.limits?.line
208
197
  lineFacts = {
209
198
  tickMs: Number(ln?.tickMs) > 0 ? Number(ln.tickMs) : lineFacts.tickMs,
@@ -218,8 +207,7 @@ const loadTerms = async () => {
218
207
  } else if (quoteMicroUSD === null) {
219
208
  spendUnit = 'base units of the paid asset'
220
209
  log(`quote: this server publishes no micro-USD price, so X402_MAX_SPEND is read as ` +
221
- `BASE UNITS OF THE ASSET, not dollars. Guessing a price here is how a spend cap ` +
222
- `silently stops capping.`)
210
+ `base units of the asset, not dollars.`)
223
211
  }
224
212
  return accepts
225
213
  }
@@ -229,7 +217,7 @@ catch (e) { log(`could not cache terms (${e.message}); falling back to probe-the
229
217
  let paymentQueue = Promise.resolve()
230
218
  const oneAtATime = (fn) => {
231
219
  const run = paymentQueue.then(fn, fn)
232
- paymentQueue = run.then(() => {}, () => {}) // a failure must not poison the queue
220
+ paymentQueue = run.then(() => {}, () => {})
233
221
  return run
234
222
  }
235
223
 
@@ -245,8 +233,6 @@ if (coldStart) log('no local channel state — probing once to learn where this
245
233
  const payNow = async (name, args) => {
246
234
  if (coldStart) {
247
235
  coldStart = false
248
- // autoPayment handles the 402 and pays the retry, and the 402 is what
249
- // carries the channel state the client is missing.
250
236
  return upstream.callTool(name, args)
251
237
  }
252
238
  const terms = name === 'tick' ? tickAccepts : accepts
@@ -258,13 +244,13 @@ const payNow = async (name, args) => {
258
244
  if (explainPermit2(out)) return out
259
245
  if (refusedPayment(out)) {
260
246
  log('payment refused as stale — dropping the local channel record and resyncing')
261
- if (channelId) { try { await watchedStorage.delete(channelId) } catch { /* it will be rebuilt */ } }
247
+ if (channelId) { try { await watchedStorage.delete(channelId) } catch {} }
262
248
  return upstream.callTool(name, args)
263
249
  }
264
250
  return out
265
251
  } catch (e) {
266
252
  if (attempt === 2) { log(`pay-first failed twice (${e.message}); using probe path`); return upstream.callTool(name, args) }
267
- try { await loadTerms() } catch { /* keep the old terms and let attempt 2 decide */ }
253
+ try { await loadTerms() } catch {}
268
254
  }
269
255
  }
270
256
  }
@@ -302,19 +288,15 @@ const AUTO_SLOW_RUN = Number(process.env.X402_AUTO_SLOW_RUN ?? 4)
302
288
 
303
289
  const line = { credential: null, socket: null, timer: null, tickMs: 250, lastUse: 0, opening: null }
304
290
 
305
- // Rolling view of how fast the caller is actually going.
306
291
  const rate = { lastCallAt: 0, fastRun: 0, slowRun: 0 }
307
292
 
308
293
  function holdingIsCheaper() {
309
294
  const now = Date.now()
310
295
  const gap = rate.lastCallAt ? now - rate.lastCallAt : Infinity
311
296
  rate.lastCallAt = now
312
- // <=, not <: at exactly one call per hold window the two cost the same, and
313
- // holding avoids a signature and a settlement per call.
314
297
  if (gap <= line.tickMs) { rate.fastRun += 1; rate.slowRun = 0 }
315
298
  else { rate.slowRun += 1; rate.fastRun = 0 }
316
299
 
317
- // Already holding? Keep holding until several gaps in a row say otherwise.
318
300
  if (line.credential) return rate.slowRun < AUTO_SLOW_RUN
319
301
  return rate.fastRun >= AUTO_FAST_RUN
320
302
  }
@@ -329,7 +311,7 @@ const wsURL = () => {
329
311
 
330
312
  const dropLine = (why) => {
331
313
  if (line.timer) { clearInterval(line.timer); line.timer = null }
332
- try { line.socket?.close() } catch { /* already gone */ }
314
+ try { line.socket?.close() } catch {}
333
315
  if (line.credential) log(`line closed (${why})`)
334
316
  line.socket = null
335
317
  line.credential = null
@@ -352,28 +334,37 @@ const tick = async () => {
352
334
 
353
335
  const openLine = () => {
354
336
  if (line.credential || line.opening) return line.opening
355
- if (!channelId) return null // no channel yet; the first paid call makes one
337
+ if (!channelId) return null
356
338
  line.opening = new Promise((resolve) => {
357
339
  let socket
358
340
  try { socket = new WebSocket(wsURL()) } catch (e) { log(`line: ${e.message}`); return resolve(null) }
359
341
  const give_up = setTimeout(() => { try { socket.close() } catch {} ; resolve(null) }, 10_000)
360
- socket.onmessage = (ev) => {
342
+ socket.onmessage = async (ev) => {
361
343
  let m; try { m = JSON.parse(String(ev.data)) } catch { return }
344
+ if (m.op === 'challenge') {
345
+ try {
346
+ const signature = await account.signMessage({ message: m.message })
347
+ socket.send(JSON.stringify({ op: 'prove', signature }))
348
+ } catch (e) {
349
+ clearTimeout(give_up); log(`line: cannot sign open challenge — ${e.message}`)
350
+ try { socket.close() } catch {}; resolve(null)
351
+ }
352
+ return
353
+ }
354
+ if (m.op === 'open_failed') {
355
+ clearTimeout(give_up); log(`line: open refused — ${m.error ?? m.why}`)
356
+ try { socket.close() } catch {}; resolve(null); return
357
+ }
362
358
  if (m.op === 'opened') {
363
359
  clearTimeout(give_up)
364
360
  line.socket = socket
365
361
  line.credential = m.credential
366
362
  line.tickMs = lineFacts.tickMs
367
- // A line that has just opened has not been idle. lastUse starts at 0, so
368
- // without this the first timer fire sees an age of Date.now() and drops
369
- // the line before anything can use it.
370
363
  line.lastUse = Date.now()
371
364
  log(`line open — ${lineFacts.microUSDPerMs ?? '?'} micro-USD/ms, ` +
372
365
  `collateral buys ${m.buysMs ?? '?'}ms`)
373
366
  const first = tick()
374
367
  line.timer = setInterval(() => {
375
- // Stop paying for a line nobody is using. The server closes an unused
376
- // line on its own; this is us not paying for the window before it does.
377
368
  if (LINE_MODE === 'auto' && Date.now() - line.lastUse > line.tickMs * 4) return dropLine('idle')
378
369
  tick()
379
370
  }, line.tickMs)
@@ -388,7 +379,7 @@ const openLine = () => {
388
379
  }
389
380
  socket.onopen = () => socket.send(JSON.stringify({ op: 'open', channelId }))
390
381
  socket.onclose = () => { clearTimeout(give_up); dropLine('socket closed'); resolve(null) }
391
- socket.onerror = () => { /* onclose follows and does the work */ }
382
+ socket.onerror = () => {}
392
383
  }).finally(() => { line.opening = null })
393
384
  return line.opening
394
385
  }
@@ -407,15 +398,6 @@ const callOnLine = async (name, args) => {
407
398
  'Raise it, set X402_MAX_SPEND=0 to remove it, or restart the bridge.' }) }] }
408
399
  }
409
400
  line.lastUse = Date.now()
410
- // TICK IS THE PAYMENT. The catalog publishes it, so a client can call it, and
411
- // it went down the line path like any other tool -- sent with a credential and
412
- // no payment, which is the one thing it cannot be. The server answered 402,
413
- // and the retry dropped a working line. It also has to share the queue with
414
- // our own ticker, or the two race and the channel refuses the loser as busy.
415
- // A TICK NEEDS A LINE TO PAY FOR. Routing it straight to payFirst kept it off
416
- // the line path, which is also the only thing that opens one -- so a client
417
- // that funded and then only ticked never got a line and every tick was
418
- // refused as matching none.
419
401
  if (name === 'tick') {
420
402
  if (!line.credential && channelId) await openLine()
421
403
  return payFirst('tick', line.credential ? { line: line.credential } : args)
@@ -489,7 +471,6 @@ if (has('--tools')) {
489
471
  if (has('--call')) {
490
472
  const tool = flag('--call')
491
473
  if (!tool) { process.stderr.write('--call needs a tool name\n'); process.exit(2) }
492
- // The JSON argument is optional: several tools take none.
493
474
  const rawArgs = argv[argv.indexOf('--call') + 2]
494
475
  let args = {}
495
476
  if (rawArgs && !rawArgs.startsWith('--')) {
@@ -512,9 +493,6 @@ if (has('--call')) {
512
493
  if (has('--refund')) {
513
494
  if (!channelId) { process.stderr.write('no channel to refund — nothing has been bought with this key and salt\n'); process.exit(2) }
514
495
 
515
- // The channel is proved by signing for it, not by holding a session open. No
516
- // line, no tick, no keep-warm: one message, one answer. Under a gift card the
517
- // key here is the payerAuthorizer, which the server accepts for the same reason.
518
496
  const issued = new Date().toISOString()
519
497
  const message = `ZEAM Prism refund\nchannel: ${String(channelId).toLowerCase()}\nissued: ${issued}`
520
498
  const signature = await account.signMessage({ message })
@@ -541,10 +519,9 @@ if (has('--refund')) {
541
519
  'The exit that always works needs nothing from us:',
542
520
  ' initiateWithdraw(config, amount) then, after the delay, finalizeWithdraw(config)',
543
521
  'We also watch for that first call and return the collateral ourselves, at our gas,',
544
- 'so you usually do not have to send the second transaction. We do NOT promise when:',
545
- 'a third-party auditor measured 920 seconds on 2026-08-26, i.e. just after the delay',
546
- 'elapsed. Plan against the delay. The escrow gates withdrawal to you alone, so your',
547
- '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.',
548
525
  '',
549
526
  ].join('\n'))
550
527
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeam-labs/x402-mcp-bridge",
3
- "version": "2.0.3",
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": {