@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.
- package/README.md +26 -32
- package/index.mjs +39 -62
- 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/
|
|
@@ -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
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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.
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
120
|
-
|
|
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.
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
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` |
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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',
|
|
@@ -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 {
|
|
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
|
|
119
|
-
let spendUnit = 'micro-USD'
|
|
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 {
|
|
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
|
-
|
|
170
|
-
|
|
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(`
|
|
177
|
-
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')
|
|
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
|
|
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
|
-
`
|
|
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(() => {}, () => {})
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 = () => {
|
|
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
|
|
545
|
-
'
|
|
546
|
-
'
|
|
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
|
+
"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": {
|