@bsvkey/inference-mcp 1.1.1 → 1.2.0
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 +12 -0
- package/package.json +2 -2
- package/server.js +61 -1
package/README.md
CHANGED
|
@@ -12,6 +12,18 @@ Two ways to pay: a **prepaid channel** (`infer`, fund once, draw down per token)
|
|
|
12
12
|
with its own key). `x402_infer` needs a funded WIF (`wif` arg or `BSVKEY_WIF`) and the
|
|
13
13
|
optional `@bsvkey/x402-bsv-client` + `@bsv/sdk` packages (installed with this one).
|
|
14
14
|
|
|
15
|
+
**Verifiable metering.** Each `infer` call returns a signed usage receipt, and the
|
|
16
|
+
tool auto-verifies it offline (with the optional packages installed): the result
|
|
17
|
+
includes `receiptVerified` and `meterVerified` (`true`, `false` + `receiptCheck`,
|
|
18
|
+
or `null` if the verifier isn't installed). It recovers the broker key (pinned
|
|
19
|
+
from `GET /v1/receipt-key`); checks channel binding, a monotonic sequence (no
|
|
20
|
+
replay/gap), and running totals within the funded amount; **recomputes the charge**
|
|
21
|
+
from the published rate (you can never be overcharged); and **recomputes the token
|
|
22
|
+
count from the exact bytes** of your system/prompt and the completion, under the
|
|
23
|
+
pinned `bsvkey-meter/1` tokenizer. So the channel payment is on-chain and both the
|
|
24
|
+
meter and the charge are auditable, without trusting the broker's word. Spec:
|
|
25
|
+
https://inference.bsvkey.com/usage-receipts.md
|
|
26
|
+
|
|
15
27
|
## Quick start
|
|
16
28
|
1. **Fund a channel once** at https://inference.bsvkey.com (BRC-100 wallet, or
|
|
17
29
|
load a key in-page). Copy the key it returns: `channelId:channelSecret`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsvkey/inference-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MCP server: buy Claude & Grok inference metered per token, settled in BSV, through the hosted inference.bsvkey.com gateway. Channels or per-call x402.",
|
|
5
5
|
"mcpName": "io.github.BSVKey/inference-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"engines": { "node": ">=18" },
|
|
10
10
|
"files": ["server.js", "SKILL.md", "README.md", "LICENSE"],
|
|
11
11
|
"optionalDependencies": {
|
|
12
|
-
"@bsvkey/x402-bsv-client": "^0.
|
|
12
|
+
"@bsvkey/x402-bsv-client": "^0.4.0",
|
|
13
13
|
"@bsv/sdk": "^2"
|
|
14
14
|
},
|
|
15
15
|
"keywords": ["mcp", "model-context-protocol", "bsv", "inference", "pay-per-token", "http-402", "x402", "claude", "grok", "micropayments", "agents"],
|
package/server.js
CHANGED
|
@@ -45,6 +45,56 @@ async function http(method, path, { headers = {}, body } = {}) {
|
|
|
45
45
|
return { ok: res.ok, status: res.status, json };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Best-effort, offline verification of the broker's signed usage receipt. Local
|
|
49
|
+
// only (a secp256k1 recovery + sha256): no payment, no network beyond a one-time
|
|
50
|
+
// key fetch, no effect on the x402 path. Gated on the optional
|
|
51
|
+
// @bsvkey/x402-bsv-client verifier so the server keeps zero REQUIRED deps: if it
|
|
52
|
+
// isn't installed we return verified:null (skipped), never an error.
|
|
53
|
+
let _verifier; // module | false | undefined
|
|
54
|
+
let _brokerKey; // hex | null | undefined
|
|
55
|
+
const _seen = new Map(); // channelId -> { seq, cumSats, cumTokens } (this session)
|
|
56
|
+
async function loadVerifier() {
|
|
57
|
+
if (_verifier !== undefined) return _verifier;
|
|
58
|
+
try { _verifier = await import('@bsvkey/x402-bsv-client/usage-receipt'); } catch { _verifier = false; }
|
|
59
|
+
return _verifier;
|
|
60
|
+
}
|
|
61
|
+
async function brokerReceiptKey() {
|
|
62
|
+
if (_brokerKey !== undefined) return _brokerKey;
|
|
63
|
+
try { const r = await http('GET', '/receipt-key'); _brokerKey = r.ok ? (r.json.receiptPubKey || null) : null; } catch { _brokerKey = null; }
|
|
64
|
+
return _brokerKey;
|
|
65
|
+
}
|
|
66
|
+
async function verifyUsageReceipt(receipt, bytes = {}) {
|
|
67
|
+
if (!receipt) return { verified: null, reason: 'no receipt returned' };
|
|
68
|
+
const V = await loadVerifier();
|
|
69
|
+
if (!V) return { verified: null, reason: 'verifier not installed (npm i @bsvkey/x402-bsv-client)' };
|
|
70
|
+
const one = await V.verifyReceipt(receipt);
|
|
71
|
+
if (!one.ok) return { verified: false, reason: one.reason };
|
|
72
|
+
const pinned = await brokerReceiptKey();
|
|
73
|
+
if (pinned && one.signer !== pinned) return { verified: false, reason: 'signer_not_pinned_broker_key' };
|
|
74
|
+
// The charge recomputes from the receipt's own rates (overcharge is a dispute).
|
|
75
|
+
if (typeof V.verifyCharge === 'function') {
|
|
76
|
+
const c = V.verifyCharge(receipt);
|
|
77
|
+
if (!c.ok) return { verified: false, reason: `charge:${c.reason}` };
|
|
78
|
+
}
|
|
79
|
+
// The meter: recompute token counts + byte digests from the exact bytes we hold.
|
|
80
|
+
let meterVerified = null;
|
|
81
|
+
if (typeof V.verifyMeter === 'function' && (bytes.prompt !== undefined || bytes.completion !== undefined)) {
|
|
82
|
+
const mv = V.verifyMeter(receipt, bytes);
|
|
83
|
+
if (!mv.ok) return { verified: false, reason: `meter:${mv.reason}`, meterVerified: false };
|
|
84
|
+
meterVerified = true;
|
|
85
|
+
}
|
|
86
|
+
if (receipt.cumSats > receipt.fundedSats) return { verified: false, reason: 'cumSats_exceeds_funded' };
|
|
87
|
+
const prev = _seen.get(receipt.channelId);
|
|
88
|
+
if (prev) {
|
|
89
|
+
// Continuity across calls this session actually observed.
|
|
90
|
+
if (receipt.seq !== prev.seq + 1) return { verified: false, reason: receipt.seq === prev.seq ? 'replayed_seq' : (receipt.seq < prev.seq ? 'seq_regressed' : 'seq_gap') };
|
|
91
|
+
if (receipt.cumSats !== prev.cumSats + receipt.sats) return { verified: false, reason: 'cumSats_does_not_reconcile' };
|
|
92
|
+
if (receipt.cumTokens !== prev.cumTokens + receipt.inputTokens + receipt.outputTokens) return { verified: false, reason: 'cumTokens_does_not_reconcile' };
|
|
93
|
+
}
|
|
94
|
+
_seen.set(receipt.channelId, { seq: receipt.seq, cumSats: receipt.cumSats, cumTokens: receipt.cumTokens });
|
|
95
|
+
return { verified: true, meterVerified, ...(prev ? {} : { note: 'baseline: signature + funded-conservation checked; seq continuity verified from here' }) };
|
|
96
|
+
}
|
|
97
|
+
|
|
48
98
|
export const TOOLS = [
|
|
49
99
|
{
|
|
50
100
|
name: 'list_models',
|
|
@@ -141,13 +191,23 @@ async function callTool(name, args = {}) {
|
|
|
141
191
|
throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
142
192
|
}
|
|
143
193
|
const x = r.json.x_bsv || {};
|
|
194
|
+
const completion = r.json.choices?.[0]?.message?.content ?? '';
|
|
195
|
+
const rc = await verifyUsageReceipt(x.usageReceipt, { system: args.system, prompt: String(args.prompt || ''), completion });
|
|
144
196
|
return {
|
|
145
197
|
model: r.json.model || args.model,
|
|
146
|
-
completion
|
|
198
|
+
completion,
|
|
147
199
|
charge: x.charge,
|
|
148
200
|
routedTo: x.routedTo,
|
|
149
201
|
balanceSatsAfter: x.balanceSatsAfter,
|
|
150
202
|
truncated: x.truncated || false,
|
|
203
|
+
// Offline-verified signed usage receipt (see usage-receipt spec). null =
|
|
204
|
+
// not checked (verifier not installed); false w/ receiptCheck = a real
|
|
205
|
+
// mismatch, treat the meter as untrusted for this call. meterVerified is
|
|
206
|
+
// true when the token count was recomputed from the exact bytes.
|
|
207
|
+
receiptVerified: rc.verified,
|
|
208
|
+
meterVerified: rc.meterVerified,
|
|
209
|
+
...(rc.reason ? { receiptCheck: rc.reason } : {}),
|
|
210
|
+
usageReceipt: x.usageReceipt,
|
|
151
211
|
};
|
|
152
212
|
}
|
|
153
213
|
case 'channel_balance': {
|