@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.
- package/README.md +9 -12
- package/index.mjs +24 -25
- 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` | `400` |
|
|
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
|
-
|
|
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,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
|
|
46
|
-
'
|
|
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 {
|
|
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
|
|
113
|
-
let spendUnit = 'micro-USD'
|
|
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 {
|
|
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
|
-
|
|
160
|
-
|
|
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(`
|
|
167
|
-
log('this key can spend
|
|
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
|
|
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
|
-
`
|
|
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(() => {}, () => {})
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 = () => {
|
|
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
|
|
523
|
-
'
|
|
524
|
-
'
|
|
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.
|
|
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": {
|