@bsvkey/inference-mcp 1.1.2 → 1.2.1

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 +8 -5
  2. package/package.json +2 -2
  3. package/server.js +27 -9
package/README.md CHANGED
@@ -14,11 +14,14 @@ optional `@bsvkey/x402-bsv-client` + `@bsv/sdk` packages (installed with this on
14
14
 
15
15
  **Verifiable metering.** Each `infer` call returns a signed usage receipt, and the
16
16
  tool auto-verifies it offline (with the optional packages installed): the result
17
- includes `receiptVerified` (`true`, `false` + `receiptCheck`, or `null` if the
18
- verifier isn't installed). It recovers the broker key (pinned from
19
- `GET /v1/receipt-key`), and checks channel binding, a monotonic sequence (no
20
- replay/gap), and running totals within the funded amount. So the channel payment
21
- is on-chain and the meter is auditable, without trusting the broker's word. Spec:
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:
22
25
  https://inference.bsvkey.com/usage-receipts.md
23
26
 
24
27
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsvkey/inference-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
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.3.0",
12
+ "@bsvkey/x402-bsv-client": "^0.4.2",
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
@@ -63,7 +63,7 @@ async function brokerReceiptKey() {
63
63
  try { const r = await http('GET', '/receipt-key'); _brokerKey = r.ok ? (r.json.receiptPubKey || null) : null; } catch { _brokerKey = null; }
64
64
  return _brokerKey;
65
65
  }
66
- async function verifyUsageReceipt(receipt) {
66
+ async function verifyUsageReceipt(receipt, bytes = {}) {
67
67
  if (!receipt) return { verified: null, reason: 'no receipt returned' };
68
68
  const V = await loadVerifier();
69
69
  if (!V) return { verified: null, reason: 'verifier not installed (npm i @bsvkey/x402-bsv-client)' };
@@ -71,6 +71,18 @@ async function verifyUsageReceipt(receipt) {
71
71
  if (!one.ok) return { verified: false, reason: one.reason };
72
72
  const pinned = await brokerReceiptKey();
73
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.messages !== undefined || 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
+ }
74
86
  if (receipt.cumSats > receipt.fundedSats) return { verified: false, reason: 'cumSats_exceeds_funded' };
75
87
  const prev = _seen.get(receipt.channelId);
76
88
  if (prev) {
@@ -80,7 +92,7 @@ async function verifyUsageReceipt(receipt) {
80
92
  if (receipt.cumTokens !== prev.cumTokens + receipt.inputTokens + receipt.outputTokens) return { verified: false, reason: 'cumTokens_does_not_reconcile' };
81
93
  }
82
94
  _seen.set(receipt.channelId, { seq: receipt.seq, cumSats: receipt.cumSats, cumTokens: receipt.cumTokens });
83
- return { verified: true, ...(prev ? {} : { note: 'baseline: signature + funded-conservation checked; seq continuity verified from here' }) };
95
+ return { verified: true, meterVerified, ...(prev ? {} : { note: 'baseline: signature + funded-conservation checked; seq continuity verified from here' }) };
84
96
  }
85
97
 
86
98
  export const TOOLS = [
@@ -162,14 +174,15 @@ async function callTool(name, args = {}) {
162
174
  case 'infer': {
163
175
  const k = keyParts(args.apiKey);
164
176
  if (!k) throw new Error('No channel key. Pass apiKey "channelId:channelSecret" or set BSVKEY_API_KEY. Open one via the open_channel tool.');
177
+ const messages = [
178
+ ...(args.system ? [{ role: 'system', content: args.system }] : []),
179
+ { role: 'user', content: String(args.prompt || '') },
180
+ ];
165
181
  const r = await http('POST', '/chat/completions', {
166
182
  headers: { authorization: `Bearer ${k.raw}` },
167
183
  body: {
168
184
  model: args.model || 'auto',
169
- messages: [
170
- ...(args.system ? [{ role: 'system', content: args.system }] : []),
171
- { role: 'user', content: String(args.prompt || '') },
172
- ],
185
+ messages,
173
186
  max_tokens: args.maxTokens || 512,
174
187
  web_search: args.webSearch === true,
175
188
  },
@@ -179,18 +192,23 @@ async function callTool(name, args = {}) {
179
192
  throw new Error(typeof msg === 'string' ? msg : JSON.stringify(msg));
180
193
  }
181
194
  const x = r.json.x_bsv || {};
182
- const rc = await verifyUsageReceipt(x.usageReceipt);
195
+ const completion = r.json.choices?.[0]?.message?.content ?? '';
196
+ // Meter over the SAME messages we sent: the OpenAI shim meters the flattened
197
+ // messages, so the verifier must reproduce that transform (needs @bsvkey/x402-bsv-client >= 0.4.2).
198
+ const rc = await verifyUsageReceipt(x.usageReceipt, { messages, completion });
183
199
  return {
184
200
  model: r.json.model || args.model,
185
- completion: r.json.choices?.[0]?.message?.content ?? '',
201
+ completion,
186
202
  charge: x.charge,
187
203
  routedTo: x.routedTo,
188
204
  balanceSatsAfter: x.balanceSatsAfter,
189
205
  truncated: x.truncated || false,
190
206
  // Offline-verified signed usage receipt (see usage-receipt spec). null =
191
207
  // not checked (verifier not installed); false w/ receiptCheck = a real
192
- // mismatch, treat the meter as untrusted for this call.
208
+ // mismatch, treat the meter as untrusted for this call. meterVerified is
209
+ // true when the token count was recomputed from the exact bytes.
193
210
  receiptVerified: rc.verified,
211
+ meterVerified: rc.meterVerified,
194
212
  ...(rc.reason ? { receiptCheck: rc.reason } : {}),
195
213
  usageReceipt: x.usageReceipt,
196
214
  };