zk-credits 0.2.5 → 0.2.8

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
@@ -2,12 +2,14 @@
2
2
 
3
3
  Loopback sidecar that attaches a hash-pinned BN254 Groth16 proof to each
4
4
  coding-agent LLM request through the experimental x402 `zk-prepaid` scheme.
5
- Built for the invite-only, unpaid, experimental Base Sepolia pilot; credits
6
- are founder-provisioned test credits, and the circuit is not independently
7
- audited.
5
+ Built for the invite-only experimental Base Sepolia pilot. Product calls use
6
+ founder-provisioned test credits and have no product payment step; the circuit
7
+ is not independently audited. External research participants in slots A and C
8
+ receive $25 for a 30-minute session regardless of setup success. That payment
9
+ is not product revenue or willingness-to-pay evidence.
8
10
 
9
11
  ```bash
10
- npm install --global zk-credits@0.2.5
12
+ npm install --global zk-credits@0.2.8
11
13
  ```
12
14
 
13
15
  ## Pinned pilot versions
@@ -17,17 +19,18 @@ not mix versions between the sidecar, the adapter package, and the gateway:
17
19
 
18
20
  | Package | Version |
19
21
  | --- | --- |
20
- | `zk-credits` (this sidecar) | `0.2.5` |
22
+ | `zk-credits` (this sidecar) | `0.2.8` |
21
23
  | `@zk-credits/x402-zk-prepaid` (adapter) | `0.1.0` |
22
24
  | `@zk-credits/shared` | `0.1.0` |
23
25
 
24
- The pinned Base Sepolia trial version is `0.2.5`. It uses the non-streaming
25
- `POST /v1/chat/completions` path below, includes the loopback Responses bridge
26
- for Codex, and acquires the pinned private proving bundle through the local
27
- GitHub CLI session. It also synchronizes V2 Base events and checks the gateway
28
- known root before local setup completes. Neither this version nor the adapter
29
- supports a Stellar or evaluation path. The proving artifacts are available
30
- only from the private immutable release pinned by `circuits/manifest.json`.
26
+ The slot A Base Sepolia trial version is `0.2.8`. It uses the non-streaming
27
+ `POST /v1/chat/completions` path below, includes the narrow loopback Responses
28
+ bridge for supported Codex text and function-tool requests, and includes the
29
+ pinned proving bundle in the package. It also
30
+ synchronizes V2 Base events and checks the gateway known root before local
31
+ setup completes. The packaged archive is checked against the immutable release
32
+ digest pinned by `circuits/manifest.json`. Version 0.2.8 must be published by
33
+ the owner before inviting a slot A participant.
31
34
 
32
35
  Verify the shipped artifacts before your first prove. The package's
33
36
  `circuits/manifest.json` fixes the SHA-256 of the frozen
@@ -57,14 +60,16 @@ still receives the class. A rejected request consumes no credit.
57
60
 
58
61
  ## First run
59
62
 
60
- 1. Redeem an invite and download the encrypted V2 credential export. Credits
61
- are founder-provisioned test credits; there is no payment step.
62
- 2. Ask the pilot contact for read access to the private bundle repository
63
- `mangekyou-labs/zk-credits-base-sepolia-v2-bundle`, then authenticate the
64
- GitHub CLI on this machine with `gh auth login`. The sidecar uses that local
65
- GitHub CLI session to read the immutable release identified in
66
- `circuits/manifest.json`; it never receives or stores your GitHub token.
67
- Set the credential, gateway, and Base Sepolia RPC endpoint:
63
+ 1. Redeem the founder-prepared invite and download the encrypted V2 credential
64
+ export. Slot A needs no access to the proving-bundle repository.
65
+ 2. Give Codex the single setup instruction in the
66
+ [first-use guide](../../docs/onboarding/base-zk-credits-codex-first-use.md).
67
+ Codex installs this pinned release, verifies its packaged bundle, and runs setup.
68
+ The human signs in if needed, enters the recovery password at the hidden
69
+ prompt, gives consent, and chooses their own task. No product payment is
70
+ involved. The sidecar needs no GitHub token to install proving material.
71
+ 3. The setup command sets the credential, gateway, and Base Sepolia RPC
72
+ endpoint:
68
73
 
69
74
  ```bash
70
75
  export ZK_CREDITS_CREDENTIAL_PATH="$HOME/Downloads/zk-credits-credential.json"
@@ -73,10 +78,10 @@ still receives the class. A rejected request consumes no credit.
73
78
  ```
74
79
 
75
80
  `zk-credits setup codex` searches the standard local folders first. If it
76
- finds no valid bundle, it downloads the pinned release, verifies the
77
- release id, tag, immutability, archive digest, and all three file digests,
81
+ finds no valid bundle, it reads the packaged archive, verifies its pinned
82
+ archive digest and all three file digests,
78
83
  then atomically installs the bundle under `~/.zk-credits/artifacts` with
79
- owner-only permissions. Missing GitHub access or any mismatch stops setup.
84
+ owner-only permissions. Any mismatch stops setup.
80
85
  It syncs public Base events from the pinned V2 bond and deployment block,
81
86
  validates that the resulting witness resolves the activated credential,
82
87
  and asks the gateway's public `POST /v1/root-known` endpoint whether the
@@ -99,7 +104,7 @@ still receives the class. A rejected request consumes no credit.
99
104
  leaves, and the local slot stays reusable. The gateway never serves a path
100
105
  for a named leaf or commitment.
101
106
 
102
- 3. Start setup. It verifies the credential, pinned artifact hashes, and
107
+ 4. Start setup. It verifies the credential, pinned artifact hashes, and
103
108
  witness locally, then prompts for the backup password without echo:
104
109
 
105
110
  ```bash
@@ -107,7 +112,9 @@ still receives the class. A rejected request consumes no credit.
107
112
  ```
108
113
 
109
114
  No gateway admin token is required. Then run `zk-credits codex` to open
110
- Codex with the local sidecar.
115
+ Codex with the local sidecar. A/C first-use measurement targets five
116
+ minutes of active human work; unattended download, proving, and provider
117
+ waits are measured separately. More than five minutes is a friction finding.
111
118
 
112
119
  The sidecar binds `127.0.0.1:3210` only. It does not modify `~/.cline` or
113
120
  `~/.codex`.
@@ -198,11 +205,20 @@ zk-credits cline [cline arguments...]
198
205
  zk-credits setup codex [--model <model>]
199
206
  zk-credits codex [codex arguments...]
200
207
  zk-credits status
201
- zk-credits serve [--port <port>]
208
+ zk-credits serve [--port <port>] [--internal-trial-one-proof]
209
+ zk-credits x402-agent
210
+ zk-credits founder-demo-agent
202
211
  zk-credits trial-registered-adapter
203
212
  eval "$(zk-credits env)"
204
213
  ```
205
214
 
215
+ `serve --internal-trial-one-proof` is an opt-in maintainer mode for the
216
+ internal Base Sepolia trial. During that process lifetime, one authenticated,
217
+ valid spend request may reach the prepaid transport and the proof factory may
218
+ attempt one proof. A later valid request receives HTTP 409 with
219
+ `internal_trial_limit_reached`; ordinary `serve` behavior is unchanged without
220
+ the flag.
221
+
206
222
  `trial-registered-adapter` is a maintainer-only internal trial command. It
207
223
  requires the locally recovered credential and pinned proving bundle, prompts
208
224
  for the credential password and gateway admin token without echo when they are
@@ -220,6 +236,17 @@ Codex SDK:
220
236
  import { buildCodexSdkOptions, buildCodexThreadOptions } from 'zk-credits/codex';
221
237
  ```
222
238
 
239
+ `zk-credits/x402` exports `createLocalX402Agent` for an operator who is
240
+ integrating the adapter into their own x402 agent. It loads the same local
241
+ credential, pinned proof artifacts, public witness, proof engine, and slot
242
+ ledger used by the sidecar. The request-aware client binds the actual method,
243
+ URL, and body into each spend; its metrics endpoint exposes authenticated
244
+ aggregate counters on loopback. See
245
+ [`docs/onboarding/base-zk-credits-x402-agent.md`](../../docs/onboarding/base-zk-credits-x402-agent.md)
246
+ for integration and measured-call steps. The included `zk-credits x402-agent`
247
+ command is a small task-running starter for the founder demo and local
248
+ rehearsal; external slot C uses their own agent.
249
+
223
250
  The credential secret is decrypted only in local memory. The sidecar cache
224
251
  contains public Base event leaves and never stores the secret, prompt, response,
225
252
  proof, account, or wallet.
Binary file
@@ -5,6 +5,7 @@ export interface AcquirePinnedBundleOptions {
5
5
  manifest: CircuitManifest;
6
6
  targetDirectory: string;
7
7
  runGithubCommand?: GithubCommand;
8
+ bundledArchivePath?: string;
8
9
  }
9
- /** Fetches a pinned private GitHub release and atomically installs its safe USTAR bundle. */
10
+ /** Installs the pinned package archive, or fetches the release for legacy/test callers. */
10
11
  export declare function acquirePinnedArtifactBundle(options: AcquirePinnedBundleOptions): Promise<string>;
@@ -5,7 +5,8 @@ import { gunzipSync } from 'node:zlib';
5
5
  import { promisify } from 'node:util';
6
6
  import { chmod, lstat, mkdir, open, readFile, rename, rm, } from 'node:fs/promises';
7
7
  import { dirname, join, resolve } from 'node:path';
8
- import { resolvePinnedArtifactBundle } from './artifact-bundle.js';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { ArtifactBundleError, resolvePinnedArtifactBundle } from './artifact-bundle.js';
9
10
  const execFile = promisify(nodeExecFile);
10
11
  const MAX_COMPRESSED_BYTES = 20 * 1024 * 1024;
11
12
  const MAX_EXPANDED_BYTES = 32 * 1024 * 1024;
@@ -126,14 +127,15 @@ function pinnedAsset(release, raw) {
126
127
  }
127
128
  const record = value;
128
129
  const assets = Array.isArray(record.assets) ? record.assets : [];
129
- const asset = assets.find((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
130
+ const matchingAssets = assets.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)
130
131
  && entry.name === release.assetName);
132
+ const asset = matchingAssets[0];
131
133
  if (record.id !== release.releaseId
132
134
  || record.tag_name !== release.tag
133
135
  || record.draft !== false
134
136
  || record.immutable !== true
135
- || !asset
136
- || (typeof asset.digest === 'string' && asset.digest !== 'sha256:' + release.sha256)) {
137
+ || matchingAssets.length !== 1
138
+ || asset.digest !== 'sha256:' + release.sha256) {
137
139
  throw new Error('Pinned proving release identity or immutability check failed');
138
140
  }
139
141
  }
@@ -185,13 +187,15 @@ async function replaceDirectoryAtomically(stage, target) {
185
187
  await rm(stage, { recursive: true, force: true });
186
188
  }
187
189
  }
188
- /** Fetches a pinned private GitHub release and atomically installs its safe USTAR bundle. */
190
+ /** Installs the pinned package archive, or fetches the release for legacy/test callers. */
189
191
  export async function acquirePinnedArtifactBundle(options) {
190
192
  const release = options.manifest.release;
191
193
  if (!release)
192
194
  throw new Error('This sidecar manifest has no pinned artifact release');
193
195
  const names = new Set(options.manifest.artifacts.map((artifact) => artifact.file));
194
196
  const command = options.runGithubCommand ?? defaultGithubCommand;
197
+ const bundledArchivePath = options.bundledArchivePath
198
+ ?? (!options.runGithubCommand ? fileURLToPath(new URL('../circuits/base-sepolia-v2.tar.gz', import.meta.url)) : undefined);
195
199
  const parent = dirname(resolve(options.targetDirectory));
196
200
  const tempDirectory = join(parent, '.base-sepolia-download-' + randomUUID());
197
201
  const stage = join(parent, '.base-sepolia-stage-' + randomUUID());
@@ -199,16 +203,49 @@ export async function acquirePinnedArtifactBundle(options) {
199
203
  await mkdir(parent, { recursive: true, mode: 0o700 });
200
204
  await mkdir(tempDirectory, { recursive: false, mode: 0o700 });
201
205
  await mkdir(stage, { recursive: false, mode: 0o700 });
202
- const releaseJson = await command(['api', `repos/${release.repository}/releases/${release.releaseId}`]);
203
- pinnedAsset(release, releaseJson);
204
- await command([
205
- 'release', 'download', release.tag,
206
- '--repo', release.repository,
207
- '--dir', tempDirectory,
208
- '--pattern', release.assetName,
209
- ]);
210
- const archivePath = join(tempDirectory, release.assetName);
211
- const archiveInfo = await lstat(archivePath);
206
+ if (!bundledArchivePath) {
207
+ try {
208
+ await command(['auth', 'status']);
209
+ }
210
+ catch {
211
+ throw new Error('GitHub CLI authentication is required; run gh auth login locally before downloading the pinned proving bundle.');
212
+ }
213
+ try {
214
+ const repositoryJson = await command(['api', `repos/${release.repository}`]);
215
+ const repository = JSON.parse(repositoryJson);
216
+ if (repository.full_name !== release.repository)
217
+ throw new Error('Repository identity mismatch');
218
+ }
219
+ catch {
220
+ throw new Error(`Pinned proving bundle requires read access to ${release.repository}; ask the founder to grant access to your signed-in GitHub account.`);
221
+ }
222
+ let releaseJson;
223
+ try {
224
+ releaseJson = await command(['api', `repos/${release.repository}/releases/${release.releaseId}`]);
225
+ }
226
+ catch {
227
+ throw new Error('Pinned proving release metadata could not be read; ask the founder to verify the published release with an authorized account. A 404 does not distinguish a missing release from insufficient access.');
228
+ }
229
+ pinnedAsset(release, releaseJson);
230
+ try {
231
+ await command([
232
+ 'release', 'download', release.tag,
233
+ '--repo', release.repository,
234
+ '--dir', tempDirectory,
235
+ '--pattern', release.assetName,
236
+ ]);
237
+ }
238
+ catch {
239
+ throw new Error('Pinned proving bundle download failed; verify gh access and the published release asset.');
240
+ }
241
+ }
242
+ const archivePath = bundledArchivePath ?? join(tempDirectory, release.assetName);
243
+ const archiveInfo = await lstat(archivePath).catch((error) => {
244
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
245
+ throw new Error('Pinned proving bundle archive is missing or too large');
246
+ }
247
+ throw error;
248
+ });
212
249
  if (archiveInfo.isSymbolicLink() || !archiveInfo.isFile() || archiveInfo.size > MAX_COMPRESSED_BYTES) {
213
250
  throw new Error('Pinned proving bundle archive is missing or too large');
214
251
  }
@@ -226,15 +263,23 @@ export async function acquirePinnedArtifactBundle(options) {
226
263
  for (const [name, bytes] of files)
227
264
  await writeBundleFile(stage, name, bytes);
228
265
  await chmod(stage, 0o700);
229
- await resolvePinnedArtifactBundle({ artifactDirectory: stage, manifest: options.manifest });
266
+ try {
267
+ await resolvePinnedArtifactBundle({ artifactDirectory: stage, manifest: options.manifest });
268
+ }
269
+ catch (error) {
270
+ if (error instanceof ArtifactBundleError && error.category === 'artifact_hash_mismatch') {
271
+ throw new Error(`Pinned proving bundle file SHA-256 check failed: ${error.message}`, { cause: error });
272
+ }
273
+ throw error;
274
+ }
230
275
  await replaceDirectoryAtomically(stage, resolve(options.targetDirectory));
231
276
  await resolvePinnedArtifactBundle({ artifactDirectory: resolve(options.targetDirectory), manifest: options.manifest });
232
277
  return resolve(options.targetDirectory);
233
278
  }
234
279
  catch (error) {
235
- if (error instanceof Error && /^(Pinned proving|This sidecar manifest|Local proving bundle)/u.test(error.message))
280
+ if (error instanceof Error && /^(Pinned proving|This sidecar manifest|Local proving bundle|GitHub CLI authentication)/u.test(error.message))
236
281
  throw error;
237
- throw new Error('Pinned proving bundle acquisition failed; verify gh access and the published release pin.');
282
+ throw new Error('Pinned proving bundle acquisition failed; verify the packaged archive and manifest pins.');
238
283
  }
239
284
  finally {
240
285
  await rm(tempDirectory, { recursive: true, force: true });
@@ -1,11 +1,11 @@
1
1
  /**
2
- * One-shot local transport for exercising the official @x402/core adapter.
2
+ * One-shot local transport for the founder's task-running x402 demo agent.
3
3
  *
4
4
  * @x402/core creates scheme payloads from PaymentRequired and does not pass
5
5
  * HTTP method/body context to a scheme client. This harness captures that
6
6
  * request context locally, then binds it into the same Base proof factory
7
7
  * before invoking the registered `zk-prepaid` scheme. It is intended only for
8
- * the internal Base Sepolia trial, not as a general HTTP client.
8
+ * the internal Base Sepolia founder demo, not as a general HTTP client.
9
9
  *
10
10
  * SPDX-License-Identifier: AGPL-3.0-or-later
11
11
  */
@@ -28,6 +28,10 @@ export interface BaseRegisteredTrialOptions extends BasePrepaidClientOptions {
28
28
  body: string;
29
29
  headers?: HeadersInit;
30
30
  fetch?: typeof fetch;
31
+ /** The response can be shown locally to the operator; it is never retained by metrics. */
32
+ onResponse?: (response: Response) => void | Promise<void>;
31
33
  }
34
+ /** A bounded, single-task request for the founder's internal x402 demo. */
35
+ export declare function buildBaseRegisteredTrialRequestBody(task?: string): string;
32
36
  /** Runs one POST to the pilot chat route through a directly registered adapter. */
33
37
  export declare function runBaseRegisteredTrialExchange(options: BaseRegisteredTrialOptions): Promise<BaseRegisteredTrialResult>;
@@ -1,17 +1,27 @@
1
1
  /**
2
- * One-shot local transport for exercising the official @x402/core adapter.
2
+ * One-shot local transport for the founder's task-running x402 demo agent.
3
3
  *
4
4
  * @x402/core creates scheme payloads from PaymentRequired and does not pass
5
5
  * HTTP method/body context to a scheme client. This harness captures that
6
6
  * request context locally, then binds it into the same Base proof factory
7
7
  * before invoking the registered `zk-prepaid` scheme. It is intended only for
8
- * the internal Base Sepolia trial, not as a general HTTP client.
8
+ * the internal Base Sepolia founder demo, not as a general HTTP client.
9
9
  *
10
10
  * SPDX-License-Identifier: AGPL-3.0-or-later
11
11
  */
12
12
  import { x402Client } from '@x402/core/client';
13
13
  import { BASE_SEPOLIA_NETWORK, decodeHeader, encodeHeader, lifecycleFailure, lifecycleStage, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PAYMENT_SIGNATURE_HEADER, registerZkPrepaidClient, ZK_PREPAID_SCHEME, } from '@zk-credits/x402-zk-prepaid';
14
14
  import { createBasePaymentFactory } from './base-sidecar.js';
15
+ /** A bounded, single-task request for the founder's internal x402 demo. */
16
+ export function buildBaseRegisteredTrialRequestBody(task = 'Reply with exactly: internal trial complete.') {
17
+ if (typeof task !== 'string' || task.trim().length === 0 || task.length > 4_000) {
18
+ throw new Error('Task must contain between 1 and 4000 characters');
19
+ }
20
+ return JSON.stringify({
21
+ model: 'deepseek/deepseek-v4-flash',
22
+ messages: [{ role: 'user', content: task }],
23
+ });
24
+ }
15
25
  function observe(options, event) {
16
26
  try {
17
27
  options.lifecycle?.(event);
@@ -163,6 +173,12 @@ export async function runBaseRegisteredTrialExchange(options) {
163
173
  }
164
174
  result.responseSucceeded = true;
165
175
  observe(options, lifecycleStage('exchange_succeeded'));
176
+ try {
177
+ await options.onResponse?.(paidResponse.clone());
178
+ }
179
+ catch {
180
+ // Displaying a completed response is local convenience, not settlement.
181
+ }
166
182
  result.committedSlotDelta = ledger.snapshot().committed - startingCommittedSlots;
167
183
  return result;
168
184
  }
@@ -50,6 +50,8 @@ export interface BasePrepaidClientOptions {
50
50
  credential: CreditCredential;
51
51
  witnessProvider: BaseWitnessProvider;
52
52
  prove: BaseProofGenerator;
53
+ /** Limits this local client to one proof attempt for a single internal trial. */
54
+ internalTrialOneProof?: boolean;
53
55
  fetch?: typeof fetch;
54
56
  /**
55
57
  * Durable local slot ledger. A slot is provisional until the proof passes
@@ -16,6 +16,12 @@ const TREE_DEPTH = 20;
16
16
  const FIELD_ORDER = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
17
17
  const REPLAY_TTL_MS = 24 * 60 * 60 * 1000;
18
18
  const MAX_PENDING_PAYMENTS = 64;
19
+ const PAYMENT_PROOF_FIELDS = ['pi_a', 'pi_b', 'pi_c'];
20
+ function proofForPayment(proof) {
21
+ return Object.fromEntries(PAYMENT_PROOF_FIELDS
22
+ .filter((field) => Object.prototype.hasOwnProperty.call(proof, field))
23
+ .map((field) => [field, proof[field]]));
24
+ }
19
25
  function field(value, label) {
20
26
  if (!/^\d+$/u.test(value))
21
27
  throw new Error(`Invalid ${label}`);
@@ -63,6 +69,7 @@ function requestBodyBytes(body) {
63
69
  /** Creates the shared local proof factory used by both client transports. */
64
70
  export function createBasePaymentFactory(options) {
65
71
  const secret = secretFromBase64Url(options.credential.secret);
72
+ let internalTrialProofAttempted = false;
66
73
  return async ({ method, url, body, requirements }) => {
67
74
  if (requirements.extra.deploymentDomain !== options.credential.deploymentDomain) {
68
75
  throw new Error('Credential deployment domain does not match the gateway challenge');
@@ -98,6 +105,11 @@ export function createBasePaymentFactory(options) {
98
105
  nullifier,
99
106
  share,
100
107
  ];
108
+ if (options.internalTrialOneProof) {
109
+ if (internalTrialProofAttempted)
110
+ throw new Error('internal_trial_limit_reached');
111
+ internalTrialProofAttempted = true;
112
+ }
101
113
  const proof = await options.prove({
102
114
  secret: secretToField(secret),
103
115
  tier_id: String(options.credential.tierId),
@@ -118,7 +130,7 @@ export function createBasePaymentFactory(options) {
118
130
  await slotLedger.commit(selectedSlot);
119
131
  const payment = buildPaymentPayload({
120
132
  requirements,
121
- proof: proof.proof,
133
+ proof: proofForPayment(proof.proof),
122
134
  publicSignals: [...proof.publicSignals],
123
135
  nonce,
124
136
  responseKey: responseKeys.publicKey,