From hl-design-systems
Kurier REST API for simplified proof verification. Use when you want the simplest integration path, don't need real-time events, or are building serverless/REST-based architectures. Covers API key setup, proof submission, status polling, and aggregation.
npx claudepluginhub horizenlabs/hl-claude-marketplace --plugin hl-design-systemsThis skill uses the workspace's default tool permissions.
Kurier provides HTTP-based proof verification - the simplest integration path for zkVerify.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Performs token-optimized structural code search using tree-sitter AST parsing to discover symbols, outline files, and unfold code without reading full files.
Kurier provides HTTP-based proof verification - the simplest integration path for zkVerify.
For real-time events or direct chain access, see zkverify-sdk or zkverify-rpc.
npm i axios dotenv# .env
KURIER_API_KEY=your_generated_api_key
KURIER_API_URL=https://api.kurier.xyz # or https://api-testnet.kurier.xyz
import axios from "axios";
import dotenv from "dotenv";
dotenv.config();
const API_URL = "https://api.kurier.xyz"; // or https://api-testnet.kurier.xyz
// Register VK (one-time)
const vkResponse = await axios.post(`${API_URL}/api/v1/register-vk/${process.env.API_KEY}`, {
proofType: "groth16",
proofOptions: { library: "snarkjs", curve: "bn128" },
vk: vkey
});
const vkHash = vkResponse.data.vkHash;
// Submit proof
const submitResponse = await axios.post(`${API_URL}/api/v1/submit-proof/${process.env.API_KEY}`, {
proofType: "groth16",
vkRegistered: true,
proofOptions: { library: "snarkjs", curve: "bn128" },
proofData: { proof, publicSignals, vk: vkHash }
});
console.log("Job ID:", submitResponse.data.jobId);
// Poll for finalization
while (true) {
const status = await axios.get(
`${API_URL}/api/v1/job-status/${process.env.API_KEY}/${submitResponse.data.jobId}`
);
if (status.data.status === "Finalized") break;
await new Promise(r => setTimeout(r, 5000));
}
Add chainId to enable aggregation for L1 verification:
const response = await axios.post(`${API_URL}/api/v1/submit-proof/${API_KEY}`, {
proofType: "groth16",
vkRegistered: true,
chainId: 8453, // Base mainnet
proofOptions: { library: "snarkjs", curve: "bn128" },
proofData: { proof, publicSignals, vk: vkHash }
});
// Poll until status === "Aggregated"
// Response includes aggregationDetails with merkleProof
chainId)Queued → Valid → Submitted → IncludedInBlock → Finalized
Finalized is terminal. The proof is on zkVerify and has a stable txHash.
chainId set — this is what you want for on-chain use)[Queued → Valid] → IncludedInBlock → AggregationPending → Aggregated → (AggregationPublished)
Queued / Valid may not be observed at a 5s polling cadence — the proof typically reaches IncludedInBlock within the first poll.IncludedInBlock = proof is in a zkVerify block. txHash and blockHash are populated from this point forward.AggregationPending = waiting to be included in an aggregation batch. This is the longest-lived state — typically 1–3 minutes.Aggregated = terminal for downstream use. aggregationDetails (with merkleProof, leaf, leafIndex, numberOfLeaves) is now populated. Use this for relay polling and recordValidation.AggregationPublished = aggregation has been published to the destination chain. You don't need to wait for this — Aggregated is sufficient.Observed timing (Base Sepolia, April 2026): ~5s to IncludedInBlock, ~25s to AggregationPending, ~165s to Aggregated. Total: ~3 minutes end-to-end (submit → Aggregated).
Important: With chainId set you typically do NOT see Finalized — the proof goes straight from AggregationPending to Aggregated. Do not wait for Finalized in the aggregation path. Stop polling as soon as you see Aggregated (or AggregationPublished).
When polling with aggregation enabled (chainId set), handle each status distinctly:
const POLL_INTERVAL_MS = 5000;
const MAX_POLL_ATTEMPTS = 120; // 10 minutes
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
const res = await fetch(`${API_URL}/api/v1/job-status/${API_KEY}/${jobId}`);
const { status, txHash, aggregationId, aggregationDetails } = await res.json();
if (status === "Failed") {
throw new Error("Proof verification failed");
}
if (status === "IncludedInBlock" || status === "AggregationPending") {
// Proof is on zkVerify but NOT yet aggregated — keep polling.
// txHash and blockHash are populated from IncludedInBlock onwards —
// save them now for explorer links + on-chain recordValidation inputs.
continue;
}
if (status === "Aggregated" || status === "AggregationPublished") {
// Terminal — aggregationDetails is now populated with the Merkle proof.
return { aggregationId, aggregationDetails, txHash, blockHash };
}
// Early states (Queued, Valid) — rarely observed at this polling cadence,
// just continue.
}
throw new Error("Polling timed out");
Common mistakes:
Finalized as terminal when chainId is set — aggregation hasn't happened yettxHash at IncludedInBlock and updating DB status to "finalized" prematurelyif block — handle each distinctly| Endpoint | Method | Description |
|---|---|---|
/api/v1/register-vk/{API_KEY} | POST | Register verification key |
/api/v1/submit-proof/{API_KEY} | POST | Submit proof for verification |
/api/v1/job-status/{API_KEY}/{jobId} | GET | Check job status |
See endpoints.md for complete API reference with request/response schemas.
| Status | StatusId | Description |
|---|---|---|
Queued | 0 | Waiting for processing |
Valid | 1 | Passed optimistic verification |
Submitted | 2 | Submitted to blockchain mempool |
IncludedInBlock | 3 | Transaction in block |
Finalized | 4 | Transaction finalized |
Failed | 5 | Processing failed |
Aggregated | 6 | Proof aggregated (requires chainId) |
AggregationPublished | 7 | Aggregation published to destination chain |
The job-status response includes additional fields as the proof progresses:
| Field | Available from | Description |
|---|---|---|
jobId | Always | Job identifier |
status | Always | Current status string |
statusId | Always | Numeric status code |
proofType | Always | Proof type (e.g., "groth16") |
chainId | Always | Destination chain (null if no aggregation) |
txHash | IncludedInBlock onwards | zkVerify extrinsic hash — use for explorer links: https://zkverify-testnet.subscan.io/extrinsic/{txHash} |
blockHash | IncludedInBlock onwards | zkVerify block hash |
aggregationId | Aggregated onwards | Sequential integer — use as attestationId in HL Marketplace contracts |
aggregationDetails | Aggregated onwards | Object with leaf, leafIndex, numberOfLeaves, merkleProof, receipt, root |
Important: txHash is the zkVerify blockchain extrinsic hash, not a Base/Ethereum transaction hash. Use it to build explorer links for proof transparency in your UI.
View finalized proofs on the zkVerify blockchain explorer:
Format: https://zkverify.subscan.io/extrinsic/{txHash}
After a proof reaches "Finalized" status, use the txHash from the job status response to view details on the zkVerify blockchain explorer. The explorer shows proof verification details, block information, transaction hashes, and gas fees.
See zkverify-explorer.md for complete details.
hl-registry-integration — Continue the pipeline: after Aggregated, wait for attestation relay on Base Sepolia and record the validation on the HL Marketplace ValidationGateway.zkverify-sdk — TypeScript SDK with real-time events (alternative to polling)zkverify-attestation — Cross-chain attestation workflowzkverify-contracts — Direct smart-contract verification