zk-credits 0.2.11 → 0.2.12

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
@@ -32,6 +32,8 @@ private: anyone with it can spend your credits.
32
32
  | `zk-credits setup codex` | Import a credential and configure Codex |
33
33
  | `zk-credits setup x402` | Prepare an x402 agent |
34
34
  | `zk-credits codex [args...]` | Start Codex with the local sidecar |
35
+ | `zk-credits codex --task-evidence` | Run a coding task and show its aggregate payment evidence on exit |
36
+ | `zk-credits investigate --chain base <transaction-hash>` | Investigate a failed Base transaction using prepaid inference |
35
37
  | `zk-credits cline [args...]` | Start Cline with the local sidecar |
36
38
  | `zk-credits x402-agent` | Run the included x402 agent |
37
39
  | `zk-credits serve` | Run the sidecar in the foreground |
@@ -82,3 +84,36 @@ Your credential stays on your device. Proofs are sent to authorize each spend. T
82
84
  can observe traffic metadata. This does not hide your IP address.
83
85
 
84
86
  Base Sepolia test credits only. The circuit is not independently audited.
87
+
88
+ ### Coding-task evidence (0.2.12 candidate)
89
+
90
+ After setup, `zk-credits codex --task-evidence` launches Codex against a fresh,
91
+ dedicated local sidecar and prints aggregate payment counters when Codex exits.
92
+ The provider and profile are fixed for attribution. The summary contains no
93
+ prompts, bearer credentials, proofs, or payment headers. `exchange_verified`
94
+ confirms a successful agent exit and an observed settled response; inspect the
95
+ code diff and tests separately to judge the coding task. The supported model and
96
+ reasoning defaults are selected automatically, including with older setups:
97
+
98
+ ```sh
99
+ zk-credits codex --task-evidence
100
+ ```
101
+
102
+ Enter your task, review the change and tests, then exit Codex to see the summary.
103
+ Version 0.2.12 is prepared for owner publication; 0.2.11 lacks this command.
104
+ See the [indexer sample](https://github.com/mangekyou-labs/haze-api/tree/main/examples/base-usdc-indexer).
105
+
106
+ ### Transaction investigator (0.2.12 candidate)
107
+
108
+ After closing Codex, run `zk-credits investigate --chain base <transaction-hash>`
109
+ with the same configured credential. It fetches public execution evidence and
110
+ uses the custom x402 `zk-prepaid` adapter for model inference. Base mainnet is
111
+ the investigated chain; credit funding remains on Base Sepolia and consumption
112
+ is offchain. It does not purchase external traces. Keep missing-evidence notices
113
+ visible and distinguish observed errors from model hypotheses.
114
+
115
+ Repeating a transaction hash recovers its existing local task; it need not make
116
+ a new paid call or return a complete report. Do not delete task journals or reset
117
+ credential accounting to force a fresh exchange. Unknown payment outcomes must
118
+ be reconciled before another paid task. A saved report must be labeled as a prior
119
+ run, with its own payment summary.
@@ -65,6 +65,28 @@ export interface BasePrepaidClientOptions {
65
65
  * a public signal, or a credential identifier.
66
66
  */
67
67
  lifecycle?: ZkPrepaidLifecycleObserver;
68
+ /** Caller-owned durable policy and accounting boundary; failures propagate. */
69
+ authorization?: BaseAuthorizationBridge;
70
+ }
71
+ export interface BaseAuthorizationRequest extends BasePaymentContext {
72
+ attemptId: string;
73
+ body: Uint8Array;
74
+ }
75
+ export type BaseUsageObservation = {
76
+ attemptId: string;
77
+ slot: number;
78
+ } & ({
79
+ type: 'provisional' | 'released' | 'retired';
80
+ } | {
81
+ type: 'settlement';
82
+ outcome: 'confirmed' | 'failed' | 'unknown';
83
+ reservationId?: string;
84
+ });
85
+ export interface BaseAuthorizationBridge {
86
+ /** Persist reservation and validate actual terms before any fresh proof. */
87
+ beforeProof(request: BaseAuthorizationRequest): Promise<void>;
88
+ /** Contains no request content, credential, proof, signature or replay key. */
89
+ observe(event: BaseUsageObservation): Promise<void>;
68
90
  }
69
91
  export interface BasePrepaidClient {
70
92
  client: ZkPrepaidClient;
@@ -80,6 +102,8 @@ export interface BasePaymentContext {
80
102
  export interface BasePreparedPayment {
81
103
  payment: PaymentPayload;
82
104
  responsePrivateKey: string;
105
+ attemptId: string;
106
+ slot: number;
83
107
  }
84
108
  export declare function validateBaseCreditWitness(witness: BaseCreditWitness): BaseCreditWitness;
85
109
  /** Creates the shared local proof factory used by both client transports. */
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * SPDX-License-Identifier: AGPL-3.0-or-later
9
9
  */
10
- import { createDecipheriv, generateKeyPair as generateKeyPairCallback, privateDecrypt, randomBytes } from 'node:crypto';
10
+ import { createDecipheriv, generateKeyPair as generateKeyPairCallback, privateDecrypt, randomBytes, randomUUID } from 'node:crypto';
11
11
  import { promisify } from 'node:util';
12
12
  import { BASE_MEMBERSHIP_TREE_CAPACITY, canonicalizeBaseJson, computeCreditLeaf, computeNullifier, computeShare, computeSlotBlinding, deriveRequestSignal, deriveSparseCreditWitness, secretFromBase64Url, secretToField, } from '@zk-credits/shared/base';
13
13
  import { buildPaymentPayload, createZkPrepaidClient, decodeHeader, PAYMENT_RESPONSE_HEADER, PAYMENT_SIGNATURE_HEADER, } from '@zk-credits/x402-zk-prepaid';
@@ -77,8 +77,13 @@ export function createBasePaymentFactory(options) {
77
77
  const slotLedger = options.slotLedger;
78
78
  if (!slotLedger)
79
79
  throw new Error('A durable slot ledger is required for the Base pilot');
80
+ const attemptId = randomUUID();
81
+ const bytes = new Uint8Array(requestBodyBytes(body));
82
+ await options.authorization?.beforeProof({ attemptId, method, url, body: bytes, requirements: structuredClone(requirements) });
80
83
  const selectedSlot = slotLedger.allocateProvisional();
84
+ let retired = false;
81
85
  try {
86
+ await options.authorization?.observe({ type: 'provisional', attemptId, slot: selectedSlot });
82
87
  const nonce = randomNonce();
83
88
  const responseKeys = await responseKey();
84
89
  const requestSignal = await deriveRequestSignal({
@@ -128,6 +133,8 @@ export function createBasePaymentFactory(options) {
128
133
  throw new Error('Proof public signals do not match the canonical statement');
129
134
  }
130
135
  await slotLedger.commit(selectedSlot);
136
+ retired = true;
137
+ await options.authorization?.observe({ type: 'retired', attemptId, slot: selectedSlot });
131
138
  const payment = buildPaymentPayload({
132
139
  requirements,
133
140
  proof: proofForPayment(proof.proof),
@@ -135,10 +142,13 @@ export function createBasePaymentFactory(options) {
135
142
  nonce,
136
143
  responseKey: responseKeys.publicKey,
137
144
  });
138
- return { payment, responsePrivateKey: responseKeys.privateKey };
145
+ return { payment, responsePrivateKey: responseKeys.privateKey, attemptId, slot: selectedSlot };
139
146
  }
140
147
  catch (error) {
141
- slotLedger.release(selectedSlot);
148
+ if (!retired) {
149
+ slotLedger.release(selectedSlot);
150
+ await options.authorization?.observe({ type: 'released', attemptId, slot: selectedSlot });
151
+ }
142
152
  throw error;
143
153
  }
144
154
  };
@@ -266,9 +276,49 @@ export function createBasePrepaidClient(options) {
266
276
  headers: new Headers(response.headers),
267
277
  });
268
278
  };
279
+ const observeSettlement = async (response, init) => {
280
+ const header = new Headers(init?.headers).get(PAYMENT_SIGNATURE_HEADER);
281
+ if (!header || !options.authorization)
282
+ return;
283
+ let payment;
284
+ try {
285
+ payment = decodeHeader(header);
286
+ }
287
+ catch {
288
+ return;
289
+ }
290
+ const key = pendingByNonce.get(payment.payload.nonce);
291
+ const pending = key ? pendingPayments.get(key) : undefined;
292
+ if (!pending)
293
+ return;
294
+ let outcome = 'unknown';
295
+ let reservationId;
296
+ try {
297
+ const encoded = response.headers.get(PAYMENT_RESPONSE_HEADER);
298
+ const result = encoded ? decodeHeader(encoded) : undefined;
299
+ if (result && result.network === pending.payment.accepted.network && result.transaction === ''
300
+ && (result.reservationId === undefined || (typeof result.reservationId === 'string' && result.reservationId.length > 0 && result.reservationId.length <= 256))
301
+ && typeof result.success === 'boolean' && !('payer' in result)) {
302
+ // A failed response can describe an ambiguous ready/commit failure.
303
+ outcome = result.success ? 'confirmed' : 'unknown';
304
+ reservationId = result.reservationId;
305
+ }
306
+ }
307
+ catch { /* Missing or malformed settlement leaves consumption unknown. */ }
308
+ await options.authorization.observe({ type: 'settlement', attemptId: pending.attemptId, slot: pending.slot, outcome,
309
+ ...(reservationId === undefined ? {} : { reservationId }) });
310
+ };
269
311
  const fetchWithReplay = async (input, init) => {
270
312
  prunePending();
271
- const response = await fetcher(input, init);
313
+ let response;
314
+ try {
315
+ response = await fetcher(input, { ...init, redirect: 'error' });
316
+ }
317
+ catch (error) {
318
+ await observeSettlement(new Response(null, { status: 503 }), init);
319
+ throw error;
320
+ }
321
+ await observeSettlement(response, init);
272
322
  if (response.status !== 409)
273
323
  return trackPaymentResponse(response, init);
274
324
  let replayAvailable = false;
@@ -343,7 +393,7 @@ export function createBasePrepaidClient(options) {
343
393
  const nonce = payment.payload.nonce;
344
394
  const expiresAt = Date.now() + REPLAY_TTL_MS;
345
395
  replayKeys.set(nonce, { privateKey: prepared.responsePrivateKey, expiresAt });
346
- pendingPayments.set(key, { payment, expiresAt });
396
+ pendingPayments.set(key, { payment, expiresAt, attemptId: prepared.attemptId, slot: prepared.slot });
347
397
  pendingByNonce.set(nonce, key);
348
398
  prunePending();
349
399
  return payment;
@@ -358,7 +408,25 @@ export function createBasePrepaidClient(options) {
358
408
  }
359
409
  },
360
410
  });
361
- return { client, committedSlots: () => options.slotLedger?.committedSlots() ?? [] };
411
+ return {
412
+ client: {
413
+ clearRequirements: client.clearRequirements,
414
+ async fetch(input, init) {
415
+ const source = input instanceof Request ? input : undefined;
416
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
417
+ const body = init?.body ?? (source?.body ? new Uint8Array(await source.clone().arrayBuffer()) : undefined);
418
+ const snapshot = body instanceof ArrayBuffer ? new Uint8Array(body.slice(0))
419
+ : body instanceof Uint8Array ? new Uint8Array(body) : body;
420
+ return client.fetch(url, {
421
+ ...(source ? { method: source.method, headers: new Headers(source.headers), signal: source.signal } : {}),
422
+ ...init,
423
+ headers: new Headers(init?.headers ?? source?.headers),
424
+ ...(snapshot === undefined ? {} : { body: snapshot }),
425
+ });
426
+ },
427
+ },
428
+ committedSlots: () => options.slotLedger?.committedSlots() ?? [],
429
+ };
362
430
  }
363
431
  function parsePublicTreeArtifact(value) {
364
432
  if (!value || typeof value !== 'object')