@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.
- package/README.md +9 -12
- package/index.mjs +28 -28
- 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)
|
|
66
|
-
|
|
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
|
-
|
|
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.
|
|
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` |
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 (
|
|
46
|
-
'
|
|
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 {
|
|
103
|
+
} catch {}
|
|
103
104
|
|
|
104
|
-
const depositPolicy =
|
|
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
|
|
113
|
-
let spendUnit = 'micro-USD'
|
|
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 {
|
|
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
|
-
|
|
160
|
-
|
|
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(`
|
|
167
|
-
log('this key can spend
|
|
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
|
|
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
|
-
`
|
|
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(() => {}, () => {})
|
|
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 {
|
|
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 {
|
|
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 ??
|
|
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 {
|
|
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
|
|
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 = () => {
|
|
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
|
|
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
|
|
523
|
-
'
|
|
524
|
-
'
|
|
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.
|
|
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": {
|