@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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/package.json +2 -2
  3. 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.1.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.2.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: r.json.choices?.[0]?.message?.content ?? '',
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': {