@bsvkey/x402-bsv-client 0.4.1 → 0.4.2

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 CHANGED
@@ -91,18 +91,28 @@ sequence has no gap or replay, the running totals reconcile
91
91
  (`cumSats`/`cumTokens`), spending never exceeds the funded amount, and each
92
92
  **charge** recomputes from the published formula (you can be charged less, never
93
93
  more). Single-receipt helpers: `verifyReceipt(receipt)` → `{ ok, signer }`,
94
- `verifyCharge(receipt)`, and `verifyMeter(receipt, { system, prompt, completion })`.
94
+ `verifyCharge(receipt)`, and `verifyMeter(receipt, { messages, completion })` (or
95
+ `{ system, prompt, completion }` on the raw `/v1/infer` path — see below).
95
96
 
96
97
  ### Verify the meter (the token count itself)
97
98
 
98
99
  The v2 receipt binds the exact bytes and is metered by a pinned, deterministic
99
- tokenizer, so you recompute the token count from what you sent and received:
100
+ tokenizer, so you recompute the token count from what you sent and received.
101
+
102
+ **On the OpenAI-compatible `/v1/chat/completions` path, pass the same `messages`
103
+ array you sent** — that endpoint meters the *flattened* messages (system joined
104
+ with `\n`; every other turn rendered as `User: …` / `Assistant: …`, joined with
105
+ `\n`), so `verifyMeter` needs the messages to reproduce that transform for you:
100
106
 
101
107
  ```js
102
108
  import { verifyMeter } from '@bsvkey/x402-bsv-client/usage-receipt';
103
- const m = verifyMeter(receipt, { system, prompt, completion }); // { ok } / { ok:false, reason }
109
+ const m = verifyMeter(receipt, { messages, completion }); // { ok } / { ok:false, reason }
104
110
  ```
105
111
 
112
+ On the broker-native `/v1/infer` path (raw fields), pass `{ system, prompt, completion }`
113
+ instead. (`messagesToPrompt(messages)` is exported if you want the flattened
114
+ `{ system, prompt }` yourself.)
115
+
106
116
  **What this proves:** the broker signed these exact numbers (non-repudiable), the
107
117
  token count is the published function of the exact bytes you exchanged, the charge
108
118
  is the published function of those tokens, none were double-counted, and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsvkey/x402-bsv-client",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Pay an x402 HTTP 402 in BSV — the missing bsv-p2pkh payment builder for x402 agents. Also verifies BSVKey inference usage receipts.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/usage-receipt.js CHANGED
@@ -62,6 +62,23 @@ export function computeClaimId(obj) {
62
62
  export function meterInputText(system, prompt) {
63
63
  return (system ? String(system) + '\n\n' : '') + String(prompt || '');
64
64
  }
65
+ // The OpenAI-compatible endpoint (/v1/chat/completions) meters the FLATTENED
66
+ // chat messages, not the raw fields: system messages are joined with '\n', and
67
+ // every other turn becomes "User: <content>" / "Assistant: <content>", joined
68
+ // with '\n'. To recompute inputDigest for a chat call, pass the SAME messages
69
+ // array you sent (verifyMeter does this for you when given { messages }). This is
70
+ // byte-identical to the broker's src/http/openai.js messagesToPrompt.
71
+ export function messagesToPrompt(messages) {
72
+ const systemParts = [], turns = [];
73
+ for (const m of (Array.isArray(messages) ? messages : [])) {
74
+ const content = typeof m.content === 'string'
75
+ ? m.content
76
+ : Array.isArray(m.content) ? m.content.map((p) => p.text || '').join('') : '';
77
+ if (m.role === 'system') systemParts.push(content);
78
+ else turns.push(`${m.role === 'assistant' ? 'Assistant' : 'User'}: ${content}`);
79
+ }
80
+ return { system: systemParts.join('\n') || undefined, prompt: turns.join('\n') };
81
+ }
65
82
  export function estimateTokens(text) {
66
83
  if (!text) return 0;
67
84
  const byChars = Math.ceil(text.length / 4);
@@ -111,8 +128,12 @@ export function verifyCharge(receipt) {
111
128
  }
112
129
 
113
130
  // The meter: recompute token counts + byte digests from the EXACT bytes you hold.
114
- export function verifyMeter(receipt, { system, prompt, completion } = {}) {
131
+ // Pass EITHER the raw { system, prompt } (broker-native /v1/infer) OR the exact
132
+ // { messages } array you sent to /v1/chat/completions (the OpenAI shim, which meters
133
+ // the flattened messages). If `messages` is given it takes precedence.
134
+ export function verifyMeter(receipt, { system, prompt, completion, messages } = {}) {
115
135
  if (receipt.meter !== METER_ID) return { ok: false, reason: `unknown_meter:${receipt.meter}` };
136
+ if (messages !== undefined) { const f = messagesToPrompt(messages); system = f.system; prompt = f.prompt; }
116
137
  const input = meterInputText(system, prompt);
117
138
  const output = String(completion || '');
118
139
  if (sha256hex(input) !== receipt.inputDigest) return { ok: false, reason: 'inputDigest_mismatch' };