<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jet Halo</title>
    <description>The latest articles on DEV Community by Jet Halo (@jethalo).</description>
    <link>https://dev.to/jethalo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F819077%2Fe04bd5c9-6c17-4c3c-8c27-749fb32e643b.jpeg</url>
      <title>DEV Community: Jet Halo</title>
      <link>https://dev.to/jethalo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jethalo"/>
    <language>en</language>
    <item>
      <title>How to Create a Zero Knowledge DApp: From Zero to Production, Case 2: zk P2P</title>
      <dc:creator>Jet Halo</dc:creator>
      <pubDate>Tue, 21 Apr 2026 13:26:25 +0000</pubDate>
      <link>https://dev.to/jethalo/how-to-create-a-zero-knowledge-dapp-from-zero-to-production-case-2-zk-p2p-3ca8</link>
      <guid>https://dev.to/jethalo/how-to-create-a-zero-knowledge-dapp-from-zero-to-production-case-2-zk-p2p-3ca8</guid>
      <description>&lt;h2&gt;
  
  
  Can a P2P on-ramp still work without a platform acting as escrow?
&lt;/h2&gt;

&lt;p&gt;In the world of crypto, the first thing a newcomer really has to do to enter the space is usually not trading and not doing DeFi, but completing an on-ramp first.&lt;/p&gt;

&lt;p&gt;And the on-ramp flow most people are familiar with is usually an OTC exchange on an exchange: the user sends fiat first, and after the platform confirms the payment, it releases USDT or USDC.&lt;/p&gt;

&lt;p&gt;The reason this works has never been just matching. The exchange holds the decision-making power in the middle. It custody-holds one side of the assets first, judges whether the off-chain payment is real, and then decides whether the crypto should be released. As long as those three things stay in the platform’s hands, users assume the trade can be completed.&lt;/p&gt;

&lt;p&gt;The question is: what happens if you remove that layer of platform-backed escrow?&lt;/p&gt;

&lt;p&gt;The buyer can send a bank transfer directly to the seller, and the seller can promise that they have enough stablecoin liquidity. But the hard parts show up immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who guarantees that the seller’s on-chain funds are actually ready&lt;/li&gt;
&lt;li&gt;Who guarantees that the buyer’s off-chain payment is real, instead of a screenshot or a forged receipt&lt;/li&gt;
&lt;li&gt;Who guarantees that this payment corresponds to the current order&lt;/li&gt;
&lt;li&gt;Who prevents the same payment proof from being reused to exchange for crypto again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason the platform model is stable is that the platform keeps handling these four things for the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without relying on an exchange to make the final decision, can a P2P fiat-to-USDC on-ramp still work, and can the final release step be turned into an on-chain release driven by verification results? This article tries to achieve that with zk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to run this case first, you can go directly to the installation steps in the &lt;a href="https://docs.zkverify.io/handbook/four-paths/3c-examples/zk-p2p" rel="noopener noreferrer"&gt;zkVerify zk-p2p example docs&lt;/a&gt;. The demo site is at &lt;a href="https://zkp2p-demo-web.vercel.app/" rel="noopener noreferrer"&gt;zkp2p-demo-web.vercel.app&lt;/a&gt;, and the GitHub repository is &lt;a href="https://github.com/JetHalo/zkp2p-demo/tree/main" rel="noopener noreferrer"&gt;JetHalo/zkp2p-demo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After reading this article, you will probably come away with three things:&lt;br&gt;
First, if a P2P fiat-to-USDC on-ramp does not rely on exchange arbitration, how the on-chain funds should be locked first;&lt;br&gt;
Second, how a Wise payment record goes through zkTLS / TLSNotary, the verifier, and &lt;code&gt;wiseReceiptHash&lt;/code&gt; to enter the proving path;&lt;br&gt;
Third, how a Noir proof generated locally in the browser eventually goes through Kurier, zkVerify, and the aggregation tuple before being consumed on-chain by &lt;code&gt;releaseWithProof(...)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Why P2P on-ramps still rely on platform-backed escrow today
&lt;/h2&gt;

&lt;p&gt;The flow on a traditional OTC platform looks smooth:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The seller posts an order&lt;/li&gt;
&lt;li&gt;The buyer places an order&lt;/li&gt;
&lt;li&gt;The buyer pays off-chain&lt;/li&gt;
&lt;li&gt;The platform confirms receipt&lt;/li&gt;
&lt;li&gt;The platform releases the crypto&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But this path works because the platform controls the state on both sides at the same time.&lt;/p&gt;

&lt;p&gt;On one side, it controls the assets in an on-chain or custodial account. The platform can hold that portion of the crypto first so it cannot move before the trade is completed. On the other side, it controls confirmation of the off-chain payment. The platform gets to decide whether a receipt, a screenshot, or a bank transfer record counts as completed payment, together with confirmation from both parties. And once there is a dispute between buyer and seller, the platform can directly arbitrate.&lt;/p&gt;

&lt;p&gt;So in the platform model, the core of the on-ramp is never “the buyer and seller agreed by themselves,” but “the platform holds the final authority to decide whether the crypto should be released.”&lt;/p&gt;

&lt;p&gt;Once the platform is removed, the real problem is no longer matching efficiency, but how to rebuild these three layers of trust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Liquidity has to be locked first&lt;/li&gt;
&lt;li&gt;Payment has to be verifiable&lt;/li&gt;
&lt;li&gt;After verification passes, the release action has to be completed by the chain itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any one of these three layers still depends on manual judgment, then the platform-backed escrow has not really been removed.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. What zkp2p is and how it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;zkp2p&lt;/code&gt; can be understood in simple terms like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The buyer pays on Wise first, the system turns that payment into a piece of verifiable evidence, and then the chain releases the USDC that the seller locked earlier on-chain to the buyer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Below, this path is unfolded step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The seller first deposits the USDC they want to sell into an on-chain pool&lt;/li&gt;
&lt;li&gt;The buyer creates an &lt;code&gt;intent&lt;/code&gt; on the page, which locks part of that liquidity first&lt;/li&gt;
&lt;li&gt;The buyer then completes a real payment on Wise&lt;/li&gt;
&lt;li&gt;The browser extension and zkTLS/TLSNotary turn that payment into an attestation&lt;/li&gt;
&lt;li&gt;The backend verifier validates the attestation and compresses it into &lt;code&gt;wiseReceiptHash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The browser locally generates a proof with &lt;code&gt;Noir + UltraHonk&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Your Web API submits the proof to Kurier and waits for zkVerify to complete verification and aggregation&lt;/li&gt;
&lt;li&gt;After the frontend gets the aggregation tuple, the buyer calls &lt;code&gt;releaseWithProof(...)&lt;/code&gt; themselves&lt;/li&gt;
&lt;li&gt;Only after contract verification passes does the previously locked USDC get sent to the buyer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the key to this path is not simply “prove that a payment happened,” but making three things true at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The funds have already been locked on-chain in advance&lt;/li&gt;
&lt;li&gt;The payment has been compressed into verification input that the later system can continue to consume&lt;/li&gt;
&lt;li&gt;The final release is not based on a platform judgment, but on-chain verification of the aggregation result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, take a look at the overall architecture diagram.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc73m0ckoz7i0t5ml80a5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc73m0ckoz7i0t5ml80a5.png" alt="Step flow diagram of the zkp2p order showing seller deposit, buyer payment, attestation capture, local proving, Kurier relay, zkVerify aggregation, and on-chain release" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are actually three different flows in this overall diagram.&lt;/p&gt;

&lt;p&gt;The first is the fund flow. The seller first puts the assets into the &lt;code&gt;Deposit Pool&lt;/code&gt;, and after the buyer completes the off-chain payment, the chain releases the corresponding portion of USDC from the pool.&lt;/p&gt;

&lt;p&gt;The second is the proof flow. The payment data on the Wise page is first captured by the zkTLS/TLSNotary plugin, then compressed by the verifier into &lt;code&gt;wiseReceiptHash&lt;/code&gt;, and then a proof is generated locally in the browser.&lt;/p&gt;

&lt;p&gt;The third is the on-chain consumption flow. The proof itself is not consumed directly by the contract. It first goes through Kurier and zkVerify and is then assembled into an aggregation tuple. The frontend reads that tuple back from the Kurier API, and the place that actually consumes and verifies it is the gateway and &lt;code&gt;DepositPool&lt;/code&gt; on the Horizen target chain.&lt;/p&gt;

&lt;p&gt;If these three flows are mixed together, it becomes easy to assume that “once the browser generates a proof, the money is released.” In reality, one of the most important design points here is this: &lt;strong&gt;there is still an independent verification and aggregation path between the proof and the payout.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  3. What components make up the system
&lt;/h2&gt;

&lt;p&gt;Before talking about sequence, first make the object boundaries clear.&lt;/p&gt;

&lt;p&gt;Because the easiest thing to mix up in this project is to treat the browser plugin, the TLSN-side artifact, your verifier service, your Web API, and Kurier / zkVerify as one big blob called “the backend.”&lt;/p&gt;

&lt;p&gt;But their responsibilities inside the system are completely different.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F45ankty0i4dmkfvi31vx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F45ankty0i4dmkfvi31vx.png" alt="Component map of zkp2p showing the seller, buyer, browser app, proof plugin, local prover, verifier services, Kurier, zkVerify, and the target chain" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After looking at this diagram, we can understand it from five directions.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;Seller&lt;/code&gt; and &lt;code&gt;Buyer&lt;/code&gt; are not directly making an on-chain transfer between each other.&lt;/p&gt;

&lt;p&gt;The seller provides liquidity by first putting funds into the on-chain &lt;code&gt;Deposit Pool&lt;/code&gt;. The buyer uses the dApp and plugin in the browser, and in the end it is also the buyer who signs the &lt;code&gt;releaseWithProof()&lt;/code&gt; transaction. The seller does not manually send crypto directly to the buyer anywhere in this path.&lt;/p&gt;

&lt;p&gt;Second, the browser itself is not a single frontend block either.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dApp&lt;/code&gt; is responsible for placing the order, creating the &lt;code&gt;intent&lt;/code&gt;, showing status, and finally providing the release entrypoint.&lt;br&gt;
The &lt;code&gt;Proof Plugin&lt;/code&gt; is responsible for capture, letting the user select the target transfer from recent payments, triggering proving, submitting the proof, and polling status.&lt;br&gt;
The &lt;code&gt;Local Prover&lt;/code&gt; is the Noir runtime and proving backend that actually runs in the browser.&lt;/p&gt;

&lt;p&gt;Third, zkTLS/TLSNotary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apps/tlsn-wise-plugin&lt;/code&gt; is responsible for producing the Wise-specific TLSN wasm artifact, while &lt;code&gt;apps/tlsn-wasm-host&lt;/code&gt; only hosts that wasm. Their job is to “generate the attestation,” not to “verify the attestation,” and even less to “submit the proof to zkVerify.”&lt;/p&gt;

&lt;p&gt;Fourth, the services are split into two layers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tlsn-verifier&lt;/code&gt;: dedicated to attestation verification, normalizing transfer fields, and producing &lt;code&gt;wiseReceiptHash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/web API&lt;/code&gt;: responsible for &lt;code&gt;/api/verify-wise-attestation&lt;/code&gt;, &lt;code&gt;/api/submit-proof&lt;/code&gt;, &lt;code&gt;/api/proof-status&lt;/code&gt;, and &lt;code&gt;/api/proof-aggregation&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fifth, &lt;code&gt;Kurier + zkVerify&lt;/code&gt; are the proof relay and the verification network.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. How the seller’s money gets locked first
&lt;/h2&gt;

&lt;p&gt;First, fix the numbers for this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;seller deposit &lt;code&gt;1000 USDC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;buyer reserve &lt;code&gt;100 USDC&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first step is &lt;code&gt;deposit()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function deposit(uint256 amount) external {
    if (amount == 0) revert InvalidAmount();

    bool ok = token.transferFrom(msg.sender, address(this), amount);
    if (!ok) revert TransferFailed();

    sellerDeposits[msg.sender] += amount;
    totalDeposited += amount;
    availableBalance += amount;

    emit Deposited(msg.sender, amount);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/Zkp2pDepositPool.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After this step, there are three new pieces of on-chain state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sellerDeposits[msg.sender]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;totalDeposited&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;availableBalance&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point there is still no order. The contract has only placed the seller’s liquidity into the pool first.&lt;/p&gt;

&lt;p&gt;The second step is &lt;code&gt;createIntent()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _createIntent(
    bytes32 intentId,
    address seller,
    uint256 amount,
    uint256 deadline,
    bytes32 nullifierHash,
    bytes32 intentHash,
    bytes32[] memory cleanupIntentIds
)
    private
{
    if (seller == address(0)) revert InvalidSeller();
    if (amount == 0) revert InvalidAmount();
    if (deadline &amp;lt;= block.timestamp) revert InvalidDeadline();

    if (availableBalance &amp;lt; amount) revert InsufficientAvailableBalance();
    if (sellerDeposits[seller] &amp;lt; sellerReserved[seller] + amount) revert SellerDepositTooLow();

    intents[intentId] = Intent({
        seller: seller,
        buyer: msg.sender,
        amount: amount,
        deadline: deadline,
        reserved: true,
        released: false,
        cancelled: false,
        nullifierHash: nullifierHash,
        intentHash: intentHash
    });

    availableBalance -= amount;
    reservedBalance += amount;
    sellerReserved[seller] += amount;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/Zkp2pDepositPool.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This step is where the order is actually written on-chain. What the contract records is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which seller this order belongs to&lt;/li&gt;
&lt;li&gt;Who the buyer address is&lt;/li&gt;
&lt;li&gt;How much the amount is&lt;/li&gt;
&lt;li&gt;When it expires&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;nullifierHash&lt;/code&gt; that must match later during release&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;intentHash&lt;/code&gt; that must match later during release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the same time, &lt;code&gt;availableBalance&lt;/code&gt; goes down, while &lt;code&gt;reservedBalance&lt;/code&gt; and &lt;code&gt;sellerReserved[seller]&lt;/code&gt; go up. That is how the &lt;code&gt;100 USDC&lt;/code&gt; is split out and locked from the seller’s total liquidity.&lt;/p&gt;

&lt;p&gt;Once this contract section is done, there is already a locked intent on-chain. The later payment proof, proof, and tuple all continue downward around this intent.&lt;/p&gt;

&lt;p&gt;The contract also prepares a recovery path when the order expires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function cancelExpiredIntent(bytes32 intentId) external {
    Intent storage intent = intents[intentId];
    if (!intent.reserved) revert IntentNotReserved();
    if (intent.released) revert IntentAlreadyReleased();
    if (intent.cancelled) revert IntentAlreadyCancelled();
    if (block.timestamp &amp;lt;= intent.deadline) revert IntentNotExpired();

    intent.reserved = false;
    intent.cancelled = true;

    reservedBalance -= intent.amount;
    availableBalance += intent.amount;
    sellerReserved[intent.seller] -= intent.amount;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/Zkp2pDepositPool.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This part returns the seller’s amount back to the available state after timeout, so later orders can continue to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The browser layer: the page prepares the order first, then hands the proving work to the plugin
&lt;/h2&gt;

&lt;p&gt;Next, go back to the page. The page first computes all the fields needed for the current order, then hands this group of fields to the plugin.&lt;/p&gt;

&lt;p&gt;The entrypoint on the page is &lt;code&gt;reserveIntentOnChain()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomHex32&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proverSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomFieldSecret&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildNullifier&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proverSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intentId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;businessDomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nonEmptyString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_BUSINESS_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zkp2p-horizen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nonEmptyString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;KURIER_API_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zkp2p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;buyerAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buyerAddress&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nowSec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;appId&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createIntent(bytes32,address,uint256,uint256,bytes32,bytes32)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;
  &lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sellerAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;nullifierHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;intentHash&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/zkp2p-horizen-release.tsx&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here the page prepares all the core fields needed later in one shot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;intentId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;intentHash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;proverSecret&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nullifierHash&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;statement&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the current implementation, &lt;code&gt;intentHash = intentId&lt;/code&gt;. This convention is carried forward later in the plugin and in the circuit as well.&lt;/p&gt;

&lt;p&gt;After the page creates the on-chain intent, it hands this order context to the plugin. The code is inside &lt;code&gt;startPluginProofWithAmount()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;intentHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;buyerAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;walletAddress&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;defaultBuyerAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amountUsdc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="na"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nonEmptyString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallbackBusinessDomain&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;aggregationDomainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KURIER_AGGREGATION_DOMAIN_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nonEmptyString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallbackAppId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reservationChainId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;fallbackChainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reservationTimestamp&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;fallbackTimestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;proverSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;verificationMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregation-kurier&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;proofSystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ultrahonk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;submitEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/submit-proof`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statusEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/proof-status`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aggregationEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/proof-aggregation`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;wiseAttestationEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_TLSN_VERIFIER_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/verify-wise-attestation`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tlsnPluginUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_TLSN_WISE_PLUGIN_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/zkp2p-horizen-release.tsx&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This puts three groups of information into the same session in one go:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The on-chain intent context&lt;/li&gt;
&lt;li&gt;The fields used by browser proving&lt;/li&gt;
&lt;li&gt;The three later backend entrypoints for submit/status/aggregation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From here, the plugin has the full proof session. The later capture, verify, prove, submit, and poll steps all proceed around this session.&lt;/p&gt;

&lt;p&gt;Mapped onto the sequence diagram, it is this section below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn29dhaiu21pq3pw27t5m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn29dhaiu21pq3pw27t5m.png" alt="Architecture diagram of the zkp2p P2P fiat-to-USDC on-ramp flow across the deposit pool, browser, verifier, Kurier, zkVerify, and the target chain" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. zkTLS / TLSNotary: first turn the Wise page into an attestation
&lt;/h2&gt;

&lt;p&gt;Next, the page hands the work to the plugin. The plugin first enters the capture stage.&lt;/p&gt;

&lt;p&gt;At this point, two concepts need to be explained clearly first: &lt;code&gt;TLSNotary&lt;/code&gt; and &lt;code&gt;wasm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TLSNotary&lt;/code&gt; can be understood first as a kind of “webpage forensics” tool. When we open the Wise page, the payment record on the page is just a string of text shown in the browser. A screenshot can capture that text too, but a screenshot cannot prove that this content really came from Wise, and it cannot prove that the content was obtained inside a real TLS session. What &lt;code&gt;TLSNotary&lt;/code&gt; is trying to do is turn “I saw this payment in the browser” into an attestation that can continue to be verified later.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wasm&lt;/code&gt; does not need to be made too complicated here. It is just a binary program that can run in the browser. The &lt;code&gt;wise.plugin.wasm&lt;/code&gt; in this project is not a normal frontend resource and not contract bytecode. It is more like a small program written specifically for the browser plugin to execute. After the plugin loads it, this wasm interacts with the Wise page according to prewritten rules, gathers the page content and TLS session material that is needed, and finally generates the attestation.&lt;/p&gt;

&lt;p&gt;So the relationship at this layer can be remembered simply like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TLSNotary&lt;/code&gt; is responsible for turning webpage content into a verifiable attestation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wise.plugin.wasm&lt;/code&gt; is the program in the browser that executes this job&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;proof-plugin&lt;/code&gt; is responsible for invoking this program inside the current order and then passing the result forward into the later verifier and proving flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these two concepts are in place, the code below will not feel abrupt.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;apps/proof-plugin/background.js&lt;/code&gt;, the core capture logic is this section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bridgeCandidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tlsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__tlsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tlsnExtension&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tlsnBridge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bridgeCandidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tlsnBridge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLSNotary runtime missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tlsnBridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runPlugin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tlsn provider missing runPlugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;tlsn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attestation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tlsnPluginUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="nx"&gt;tlsn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/proof-plugin/background.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tlsnPluginUrl&lt;/code&gt; here points to &lt;code&gt;wise_plugin.tlsn.wasm&lt;/code&gt;. This wasm comes from the cooperation of two directories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apps/tlsn-wise-plugin&lt;/code&gt;: produces the Wise-specific TLSN plugin artifact&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/tlsn-wasm-host&lt;/code&gt;: exposes this wasm as a URL that the browser can access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This layer then proceeds in the following four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;apps/tlsn-wise-plugin&lt;/code&gt; produces &lt;code&gt;wise.plugin.wasm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/tlsn-wasm-host&lt;/code&gt; hosts it&lt;/li&gt;
&lt;li&gt;The plugin executes it on the Wise page through &lt;code&gt;runPlugin()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The browser gets the attestation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After capture ends, the plugin pushes the result together with several recent payments into the proof session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;amountText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recipientText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;transferTimeText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pageTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recentTransfers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;tlsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;capturedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/proof-plugin/background.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After capture ends, the plugin holds two kinds of things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tlsn.attestation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;recentTransfers&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first is used by the verifier later, and the second is used in the popup so the user can select the payment corresponding to the current order.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;tlsn-verifier&lt;/code&gt;: verify the attestation first, then compress it into &lt;code&gt;wiseReceiptHash&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The next step enters &lt;code&gt;apps/tlsn-verifier&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This service first performs local TLSN verification. The core code is in &lt;code&gt;src/lib.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;presentationHex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractPresentationHex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;presentationHex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unable to extract presentation hex from attestation payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;verifyPresentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;presentationHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notaryPublicKeyPem&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;rawResult&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;rawResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;rawResult&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;recv&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;rawResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recv&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;serverName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pickString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;asRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawResult&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serverName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sourceHost&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/tlsn-verifier/src/lib.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After local verification completes, the service has already obtained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;presentationHex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sent&lt;/code&gt; / &lt;code&gt;recv&lt;/code&gt; from the TLS session&lt;/li&gt;
&lt;li&gt;The connected server host&lt;/li&gt;
&lt;li&gt;The session timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then in &lt;code&gt;src/server.js&lt;/code&gt;, the service matches the transfer selected by the user against recent transfers and then normalizes the fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;localVerification&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyPresentationLocally&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;notaryPublicKeyPem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;verifyPresentation&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recentTransfers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractRecentTransfers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attestationRaw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;localVerification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recentCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedTransfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;findMatchingRecentTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recentTransfers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedTransfer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;normalizedCheck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;buildWiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/tlsn-verifier/src/server.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What the service returns to the frontend is a group of stable fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;amount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;payerRef&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transferId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sourceHost&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wiseReceiptHash&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;wiseReceiptHash&lt;/code&gt; then continues into the later circuit inputs, Web API anti-replay checks, and proof submit.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. &lt;code&gt;apps/web API&lt;/code&gt;: what these four API routes each do
&lt;/h2&gt;

&lt;p&gt;This &lt;code&gt;apps/web&lt;/code&gt; layer corresponds to four API routes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/api/verify-wise-attestation&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This route forwards the attestation from the browser to the verifier, and then aligns it once more with the order context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nf"&gt;isUnsignedIntegerText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nf"&gt;isUnsignedIntegerText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amount mismatch with expected order amount&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedTransferId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expectedTransferId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transferId mismatch with selected payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attestationDigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attestation&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceHost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;normalizedTransferId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payerRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="nx"&gt;attestationDigest&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/api/verify-wise-attestation.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This puts three things together and checks them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;amount&lt;/code&gt; in the current order&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;transferId&lt;/code&gt; selected by the current user&lt;/li&gt;
&lt;li&gt;The normalized payment fields returned by the verifier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the check passes, it returns &lt;code&gt;wiseReceiptHash&lt;/code&gt; to the plugin.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/api/submit-proof&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;After the plugin generates the proof locally, it sends the proof and public inputs to this route.&lt;/p&gt;

&lt;p&gt;This route first performs one anti-replay layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierReserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reserveNullifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;nullifierReserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anti-replay violation: nullifier already seen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wiseReceiptReserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reserveWiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptReserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;releaseNullifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anti-replay violation: wise receipt hash already seen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/api/submit-proof.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The in-memory store used here is in &lt;code&gt;proof-store.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wiseReceiptHashSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusByProofId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProofStatusResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tupleByProofId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProofAggregationTuple&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/zkp2p-horizen-release/store/proof-store.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then this route converts the payload into a body that Kurier accepts and tries several compatible endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;buildKurierSubmitBodies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;buildSubmitTargets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;upstream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;postSubmitCandidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/api/submit-proof.ts&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/api/proof-status&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;After the proof enters Kurier, this route maps the upstream job status into the few statuses needed by the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mapProofStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregationpublished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;included&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finalized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregation pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregationpending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/zkp2p-horizen-release/api/kurier.ts&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/api/proof-aggregation&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;After the proof enters the aggregation stage, this route extracts the tuple that will be consumed on-chain from the job status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tupleFromJobStatusRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;aggregationDomainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;intentHash&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ProofAggregationTuple&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationDetails&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aggregationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leafCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;numberOfLeaves&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leafCount&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leafIndex&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merklePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merkleProof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merkleProof&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/api/proof-aggregation.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What the browser is waiting for at this stage is exactly this tuple.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. The &lt;code&gt;Noir&lt;/code&gt; circuit: the browser loads the artifact and generates the proof locally with &lt;code&gt;UltraHonk&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The core code for local proving is in &lt;code&gt;browser-prover.ts&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;installBrowserProver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initNoirWasmRuntime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchCircuitArtifact&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;noir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Noir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UltraHonkBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circuit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bytecode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ultraHonkOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;keccak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__ZKP2P_NOIR_PROVER__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;prove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WitnessPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProofResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;witness&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;noir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;circuitInputs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;witness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ultraHonkOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;locallyVerified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ultraHonkOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;locallyVerified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Local UltraHonk verification failed (oracle hash / VK mismatch)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toHex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizePublicInputToHex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/zkp2p-horizen-release/browser-prover.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The actual runtime assets used here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Circuit source file: &lt;code&gt;circuits/zkp2p-horizen-release/noir/src/main.nr&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Artifact loaded by the frontend: &lt;code&gt;/api/circuit-artifact?name=zkp2p_horizen_release&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Proving backend: &lt;code&gt;UltraHonkBackend&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verification key: &lt;code&gt;circuits/zkp2p-horizen-release/noir/target/vk&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This runtime model is different from the usual &lt;code&gt;circom wasm + zkey&lt;/code&gt; frontend mental model. What the page gets here is a Noir artifact JSON; what is initialized locally is &lt;code&gt;Noir + ACVM + bb.js&lt;/code&gt;; and what comes out in the end is &lt;code&gt;proof + publicInputs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the plugin actually triggers proving, it is calling exactly this browser runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wise receipt hash missing, run capture first&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;patchSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SESSION_STATUS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROVING&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proving&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runBrowserProving&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capture&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;patched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;patchSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proving&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proving&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SESSION_STATUS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROOF_READY&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/proof-plugin/background.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;At this point, the proof has been fully generated inside the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. What exactly the circuit binds: &lt;code&gt;statement&lt;/code&gt;, &lt;code&gt;nullifier&lt;/code&gt;, &lt;code&gt;wiseReceiptHash&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Next, unfold the circuit itself.&lt;/p&gt;

&lt;p&gt;First, unfold the inputs and constraints of &lt;code&gt;main()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;business_domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_addr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;chain_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wise_receipt_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wise_witness_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;expected_statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;business_domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user_addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;chain_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected_statement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;expected_nullifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_nullifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intent_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullifier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected_nullifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;expected_wise_witness_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nf"&gt;compute_wise_witness_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wise_receipt_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wise_witness_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected_wise_witness_hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/zkp2p-horizen-release/noir/src/main.nr&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This code makes the relationship in the current circuit very clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The public inputs carry the order context&lt;/li&gt;
&lt;li&gt;The private inputs carry &lt;code&gt;secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;statement&lt;/code&gt; must be recomputable from the order context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nullifier&lt;/code&gt; must be recomputable from &lt;code&gt;secret + intent_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wise_witness_hash&lt;/code&gt; must be recomputable from the payment fields and &lt;code&gt;secret&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First look at &lt;code&gt;statement&lt;/code&gt;. The TS side and the circuit keep the same order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildStatementField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StatementInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;businessDomain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringToField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stringToField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userAddr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hexToField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buyerAddress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hexToField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chainId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mimc7Hash2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userAddr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mimc7Hash2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mimc7Hash2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mimc7Hash2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mimc7Hash2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;mimc7Hash2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/zkp2p-horizen-release/statement.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once this order is fixed, &lt;code&gt;statement&lt;/code&gt; folds the following into one value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;intentId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;buyerAddress&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;amount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chainId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;businessDomain&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;appId&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, look at how the plugin assembles the witness. &lt;code&gt;deriveCircuitInputs()&lt;/code&gt; arranges the fields in the session into circuit inputs, and it also preserves one current implementation detail here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentIdHex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeHex32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;intentId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentHashHex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeHex32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentHash&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;intentHash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intentHashHex&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;intentIdHex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`intentHash mismatch: intentHash=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;intentHashHex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; intentId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;intentIdHex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Current circuit binds intentHash to intentId.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/proof-plugin/lib/circuit-inputs.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This shows that in the current version, the actual binding of &lt;code&gt;intentHash&lt;/code&gt; is still equal to &lt;code&gt;intentId&lt;/code&gt;. The page, the plugin, and the contract all keep using this assumption.&lt;/p&gt;

&lt;p&gt;Finally, look at &lt;code&gt;wiseWitnessHash&lt;/code&gt;. The plugin folds the payment fields together with &lt;code&gt;wiseReceiptHash&lt;/code&gt; one more time and then places the result into the private inputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wiseWitnessHashField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeWiseWitnessHash&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;amountField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timestampField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;userAddrField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;wiseReceiptHashField&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;secretField&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privateInputsByName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;secretField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;wise_witness_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wiseWitnessHashField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/proof-plugin/lib/circuit-inputs.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;By the end of this circuit section, the order context, the payment digest, and the one-time consumption right have all been placed into the same witness relation.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. After the proof leaves the browser, how the frontend waits for results from Kurier and zkVerify
&lt;/h2&gt;

&lt;p&gt;After the browser gets &lt;code&gt;proof + publicInputs&lt;/code&gt;, the plugin immediately submits them to &lt;code&gt;/api/submit-proof&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;verificationMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verificationMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;proofSystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proofSystem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aggregationDomainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationDomainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userAddr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buyerAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intentHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentHash&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;wiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;postJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submitEndpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/proof-plugin/background.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This section deserves to be discussed in more detail, because from this point on, the proof has already left the browser, and the later flow enters the full &lt;code&gt;Kurier -&amp;gt; zkVerify -&amp;gt; aggregation tuple&lt;/code&gt; path.&lt;/p&gt;

&lt;p&gt;First, separate these two roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Kurier&lt;/code&gt; is the relay and status outlet. It processes the proof payload forwarded by your backend, returns &lt;code&gt;providerJobId&lt;/code&gt;, exposes job status and the aggregation tuple, and synchronizes zkVerify’s results into the target-chain verification path.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;zkVerify&lt;/code&gt; is the verification and aggregation network. After the proof enters this layer, it goes through verification and aggregation, eventually corresponding to an aggregation result that the target-chain gateway can verify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things should be remembered here first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the frontend polls later is actually the upstream job corresponding to &lt;code&gt;providerJobId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Even though the page reads the tuple back from the Kurier API, the place that actually executes &lt;code&gt;verifyProofAggregation(...)&lt;/code&gt; and &lt;code&gt;releaseWithProof()&lt;/code&gt; is the Horizen target chain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;/api/submit-proof&lt;/code&gt; first performs one round of business-field validation, and then submits this proof upward together with the current order context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appId mismatch with server env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationDomainId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationDomainId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregationDomainId mismatch with server env&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierReserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reserveNullifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;nullifierReserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anti-replay violation: nullifier already seen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wiseReceiptReserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reserveWiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptReserved&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;releaseNullifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anti-replay violation: wise receipt hash already seen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/api/submit-proof.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This step makes two layers of constraints explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;appId&lt;/code&gt; and &lt;code&gt;aggregationDomainId&lt;/code&gt; inside the proof must match the current backend environment&lt;/li&gt;
&lt;li&gt;The same &lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;wiseReceiptHash&lt;/code&gt; cannot be submitted twice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the validation passes, the backend packages the proof into a body accepted by Kurier. It explicitly carries &lt;code&gt;vk&lt;/code&gt;, &lt;code&gt;proof&lt;/code&gt;, and &lt;code&gt;publicSignals&lt;/code&gt;, while also preserving the order-related fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;proofType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proofSystem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;proofOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;vkRegistered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;proofData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;vk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vkHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalizedProof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;publicSignals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalizedPublicSignals&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verificationMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;businessDomain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;aggregationDomainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationDomainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userAddr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intentHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/api/submit-proof.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The meaning here is very clear: what gets submitted to Kurier is a proof payload that has already been bound to the current order. The proof system, vk, public signals, intent context, and nullifier all enter upstream together at this layer.&lt;/p&gt;

&lt;p&gt;After the backend submits successfully, it pulls &lt;code&gt;providerJobId&lt;/code&gt; out of the upstream response and binds the local &lt;code&gt;proofId&lt;/code&gt; to that upstream job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providerJobId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractProviderJobId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;providerJobId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;releaseNullifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;releaseWiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wiseReceiptHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kurier submit response missing jobId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProofStatusResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rawStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kurier-keyed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;availableKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;providerJobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;intentHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intentHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/api/submit-proof.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After this step, the page holds two sets of IDs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The local &lt;code&gt;proofId&lt;/code&gt; of this proof session&lt;/li&gt;
&lt;li&gt;The upstream Kurier / zkVerify job &lt;code&gt;providerJobId&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is the second one that is actually used later to query status.&lt;/p&gt;

&lt;p&gt;After successful submission, the plugin saves &lt;code&gt;submitResponse&lt;/code&gt; and starts polling status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusEndpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;statusUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;proofId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;providerJobId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;providerJobId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statusUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;providerJobId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerJobId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusResp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/proof-plugin/background.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/api/proof-status&lt;/code&gt; first uses &lt;code&gt;providerJobId&lt;/code&gt; to query Kurier job status, and then maps the upstream status into a few more stable statuses for the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mapProofStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregationpublished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;included&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finalized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregation pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aggregationpending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/zkp2p-horizen-release/api/kurier.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The status here can be understood as having two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rawStatus&lt;/code&gt; preserves the raw upstream wording from Kurier / zkVerify, which is useful for debugging&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pending / verified / aggregated / failed&lt;/code&gt; is the page’s own stable state machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The page itself also polls along with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;queryStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeProofId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;statusPayload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusResponse&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;rawStatus&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
  &lt;span class="nx"&gt;statusPayload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusResponse&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
  &lt;span class="nx"&gt;statusPayload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;applyProofStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeProofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawStatus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/zkp2p-horizen-release.tsx&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After the status reaches &lt;code&gt;aggregated&lt;/code&gt;, the plugin then fetches the aggregation tuple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tupleUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationEndpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;tupleUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;proofId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;providerJobId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;providerJobId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;tupleUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;providerJobId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;providerJobId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tupleResp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tupleUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;patchSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tupleResp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/proof-plugin/background.js&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/api/proof-aggregation&lt;/code&gt; extracts the aggregation result produced by zkVerify from the job status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tupleFromJobStatusRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;aggregationDomainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;intentHash&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ProofAggregationTuple&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationDetails&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;details&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aggregationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregationId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leaf&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leafCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;numberOfLeaves&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leafCount&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leafIndex&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merklePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merkleProof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merkleProof&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/api/proof-aggregation.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This layer is where zkVerify’s aggregation result is finally organized into the format consumed by the target chain. All the statuses, logs, and job information returned earlier by Kurier and zkVerify are eventually reduced to this set of fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aggregationDomainId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aggregationId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leaf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;merklePath&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leafCount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the inputs that the target-chain gateway &lt;code&gt;verifyProofAggregation(...)&lt;/code&gt; can actually understand.&lt;/p&gt;

&lt;p&gt;What the page really uses later from the tuple is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;aggregationDomainId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aggregationId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leaf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;merklePath&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leafCount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Chapter 11, the relationship to remember first is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The browser is responsible for generating the proof locally&lt;/li&gt;
&lt;li&gt;The Web API is responsible for binding the proof to the business fields and then submitting it to Kurier&lt;/li&gt;
&lt;li&gt;Kurier sends this proof into zkVerify’s verification / aggregation flow, and exposes job status / tuple to the frontend&lt;/li&gt;
&lt;li&gt;After the frontend gets the tuple, the actual verification happens on the gateway / deposit pool of the Horizen target chain&lt;/li&gt;
&lt;li&gt;What the page is waiting for in the end is this tuple, not a piece of status text&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after this proof relay section is complete does the page truly have the parameters needed to call &lt;code&gt;releaseWithProof()&lt;/code&gt; on-chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. How the final &lt;code&gt;releaseWithProof()&lt;/code&gt; happens
&lt;/h2&gt;

&lt;p&gt;Finally, go back to the page and the contract.&lt;/p&gt;

&lt;p&gt;Before the buyer signs, the page has already arranged the tuple and the intent-related fields. The place where the transaction is actually sent is in &lt;code&gt;apps/web/pages/zkp2p-horizen-release.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;appendPluginLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;releaseWithProof 提交中...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;releaseWithProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;intentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;proofIntentHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;domainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;aggregationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;leaf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;leafCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;index&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;appendPluginLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`release tx: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receipt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/pages/zkp2p-horizen-release.tsx&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After the contract receives this set of parameters, it checks them in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Intent storage intent = intents[intentId];
if (!intent.reserved) revert IntentNotReserved();
if (intent.released) revert IntentAlreadyReleased();
if (intent.cancelled) revert IntentAlreadyCancelled();
if (block.timestamp &amp;gt; intent.deadline) revert IntentExpired();
if (msg.sender != intent.buyer) revert OnlyIntentBuyer();
if (nullifierUsed[nullifierHash]) revert NullifierAlreadyUsed();
if (intent.nullifierHash != nullifierHash) revert NullifierMismatch();
if (intent.intentHash != proofIntentHash) revert IntentHashMismatch();

bool verified = gateway.verifyProofAggregation(domainId, aggregationId, leaf, merklePath, leafCount, index);
if (!verified) revert VerificationFailed();

nullifierUsed[nullifierHash] = true;
intent.reserved = false;
intent.released = true;

bool transferred = token.transfer(intent.buyer, intent.amount);
if (!transferred) revert TransferFailed();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/Zkp2pDepositPool.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After the transaction is confirmed, both the on-chain state and the funds land at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nullifierHash&lt;/code&gt; is marked as used&lt;/li&gt;
&lt;li&gt;The current intent changes from &lt;code&gt;reserved&lt;/code&gt; to &lt;code&gt;released&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The seller’s locked amount is reduced&lt;/li&gt;
&lt;li&gt;The USDC corresponding to &lt;code&gt;intent.amount&lt;/code&gt; is transferred to the buyer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, the entire path is closed.&lt;/p&gt;

&lt;p&gt;Each earlier layer does one thing of its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The contract locks the seller’s liquidity out first&lt;/li&gt;
&lt;li&gt;The page prepares the order context&lt;/li&gt;
&lt;li&gt;The plugin obtains the attestation from the Wise page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tlsn-verifier&lt;/code&gt; verifies the attestation and compresses it into &lt;code&gt;wiseReceiptHash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The browser locally generates the proof with &lt;code&gt;Noir + UltraHonk&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The Web API sends the proof into Kurier and zkVerify&lt;/li&gt;
&lt;li&gt;After the page gets the aggregation tuple, the buyer initiates &lt;code&gt;releaseWithProof()&lt;/code&gt; themselves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That way, this full P2P fiat-to-USDC on-ramp path lands end to end. The seller locks liquidity first, the buyer then completes the Wise payment, the browser pushes the payment evidence into proving, Kurier synchronizes zkVerify’s aggregation result outward, and after the frontend gets the tuple, validation and release are finally completed by &lt;code&gt;releaseWithProof()&lt;/code&gt; on the Horizen target chain.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>beginners</category>
      <category>blockchain</category>
      <category>zk</category>
    </item>
    <item>
      <title>How to Create a Zero Knowledge DApp: From Zero to Production, Case 1: zk Escrow</title>
      <dc:creator>Jet Halo</dc:creator>
      <pubDate>Tue, 21 Apr 2026 12:54:18 +0000</pubDate>
      <link>https://dev.to/jethalo/how-to-create-a-zero-knowledge-dapp-from-zero-to-production-case-1-zk-escrow-1kgj</link>
      <guid>https://dev.to/jethalo/how-to-create-a-zero-knowledge-dapp-from-zero-to-production-case-1-zk-escrow-1kgj</guid>
      <description>&lt;p&gt;This article is about understanding an end-to-end zk application from a full-stack, end-to-end perspective.&lt;/p&gt;

&lt;p&gt;It uses the &lt;a href="https://github.com/JetHalo/zk-Escrow" rel="noopener noreferrer"&gt;&lt;code&gt;ZK Escrow Release&lt;/code&gt;&lt;/a&gt; repository as the concrete example, and walks through one escrow flow from the business problem, to the contract, the circuit, the frontend, the verification layer, and finally on-chain consumption.&lt;/p&gt;

&lt;p&gt;The goal here is not to walk through the code line by line, and it is not to repeat the install, run, and deployment commands. Those are better kept in step-by-step docs. This article is focused on a different question: if you want to build a complete zk app yourself, what should you think about first, what should you build next, what is each layer responsible for, and where exactly zkVerify fits in the whole path.&lt;/p&gt;

&lt;p&gt;This article will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where this escrow project is similar to Tornado Cash, and where it is not&lt;/li&gt;
&lt;li&gt;what parts usually make up a complete zk app&lt;/li&gt;
&lt;li&gt;what the contract, circuit, frontend, backend, and verification layer each do&lt;/li&gt;
&lt;li&gt;how to think about zkVerify once it is part of the system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If, by the end, the reader can explain from a full-system perspective which parts a zk app needs, why those parts exist, how they connect to each other, and why zkVerify appears at the verification layer, then this article has done its job.&lt;/p&gt;

&lt;p&gt;You can treat it as a development map, not as an operations manual.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Suggested reading approach&lt;/strong&gt;&lt;br&gt;
It is best to first follow the zkVerify installation guide and get the project running locally, then read this article alongside the code. The installation steps are here: &lt;a href="https://docs.zkverify.io/handbook/quickstart/tutorial-01-operations-only" rel="noopener noreferrer"&gt;Tutorial 01: Operations Only&lt;/a&gt;. The code repository used in this tutorial is here: &lt;a href="https://github.com/JetHalo/zk-Escrow" rel="noopener noreferrer"&gt;&lt;code&gt;ZK Escrow Release&lt;/code&gt;&lt;/a&gt;. That way, when you reach each section, you can jump straight to the matching module in the repo and compare the contract, circuit, frontend, and API together. It makes the whole end-to-end flow much easier to understand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Understanding What Tornado Cash Is
&lt;/h2&gt;

&lt;p&gt;Tornado Cash had a huge influence on ZK. Even today, many people first think of the controversy around it, but for developers, what it really left behind is a way of thinking:&lt;/p&gt;

&lt;p&gt;you do not have to hand the secret directly to the chain in order for the chain to accept a valid spend.&lt;/p&gt;

&lt;p&gt;That is why it is still hard to talk about how a zk app is built without talking about Tornado Cash first. Not because this article is trying to retell Tornado Cash itself, but because many later ZK applications, especially projects that use structures like &lt;code&gt;commitment&lt;/code&gt;, &lt;code&gt;nullifier&lt;/code&gt;, and a Merkle tree, are easiest to understand when you start from there.&lt;/p&gt;

&lt;p&gt;Of course, Tornado Cash is not only those pieces. It is a complete protocol with its own deposit pools, withdrawal flow, verifier, relayer, and frontend interaction. Here, the point is to focus on its core cryptographic skeleton, because the escrow project in this repo borrows that skeleton while changing the business goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Tornado Cash Actually Does
&lt;/h2&gt;

&lt;p&gt;Start with Tornado Cash.&lt;/p&gt;

&lt;p&gt;If you compress its core idea into one sentence, it looks like this:&lt;/p&gt;

&lt;p&gt;a user deposits assets into a pool and gets a note that only they know. Later, when withdrawing, the user does not reveal the original deposit directly to the chain. Instead, they use that note to generate a proof and show the contract that they really do have the right to withdraw from the pool.&lt;/p&gt;

&lt;p&gt;The key point is that the funds can finally be withdrawn to a new address. The contract knows the withdrawal is valid, but it does not know which original deposit inside the pool it came from. What Tornado Cash is really doing is cutting that link.&lt;/p&gt;

&lt;p&gt;To do that, Tornado Cash naturally grows a structure like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generate a note at deposit time&lt;/li&gt;
&lt;li&gt;derive a &lt;code&gt;commitment&lt;/code&gt; from that note&lt;/li&gt;
&lt;li&gt;place all commitments into a Merkle tree&lt;/li&gt;
&lt;li&gt;at withdrawal time, use a proof to show “I know the secret behind one commitment, and that commitment belongs to this tree”&lt;/li&gt;
&lt;li&gt;use a &lt;code&gt;nullifier&lt;/code&gt; or &lt;code&gt;nullifier hash&lt;/code&gt; to prevent the same credential from being spent twice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, Tornado Cash already gives enough of a reference model for the rest of this article.&lt;/p&gt;

&lt;p&gt;Now look at this project.&lt;/p&gt;

&lt;p&gt;This project borrows the same cryptographic skeleton, but it is not doing anonymous withdrawals.&lt;/p&gt;

&lt;p&gt;The flow here is more direct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user A deposits funds into the contract&lt;/li&gt;
&lt;li&gt;at deposit time, the recipient address &lt;code&gt;B&lt;/code&gt; is written on-chain&lt;/li&gt;
&lt;li&gt;the frontend first generates a credential locally and computes the corresponding &lt;code&gt;commitment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;later, whoever holds that credential and successfully proves it can trigger release&lt;/li&gt;
&lt;li&gt;but the funds can only go to the originally bound &lt;code&gt;B&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the most fundamental difference from Tornado Cash.&lt;/p&gt;

&lt;p&gt;In Tornado Cash, the withdrawal address can be a fresh new address.&lt;/p&gt;

&lt;p&gt;This project does not work like that. Here, the recipient is already locked in when the deposit happens. The later proof is not deciding who gets paid. It is proving that the funds can now be released to the address that was bound from the beginning.&lt;/p&gt;

&lt;p&gt;So even though both systems contain &lt;code&gt;commitment&lt;/code&gt;, &lt;code&gt;nullifier&lt;/code&gt;, a Merkle tree, and a proof, those pieces are serving different goals.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsprkh6vwg8u5pmsr44ue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsprkh6vwg8u5pmsr44ue.png" alt="Tornado Cash core flow diagram showing deposit, private note, commitment, nullifier hash, proof, and withdrawal to a new address" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconstructing the Product Flow from the User's Point of View
&lt;/h2&gt;

&lt;p&gt;Do not rush into the circuit yet, and do not rush into zkVerify yet either.&lt;/p&gt;

&lt;p&gt;First, walk through one concrete transfer inside this project. After that, it becomes much easier to come back and break down what each layer is doing.&lt;/p&gt;

&lt;p&gt;Suppose we have this escrow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user A deposits &lt;code&gt;0.1 ETH&lt;/code&gt; into the contract&lt;/li&gt;
&lt;li&gt;at deposit time, the recipient address &lt;code&gt;B&lt;/code&gt; is written on-chain together with it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this step, the chain remembers two kinds of things.&lt;/p&gt;

&lt;p&gt;The first kind is the escrow's own data, such as the amount, the recipient, and whether the funds have already been spent.&lt;/p&gt;

&lt;p&gt;The second kind is data related to the proof, namely the &lt;code&gt;commitment&lt;/code&gt; derived from the credential. That &lt;code&gt;commitment&lt;/code&gt; is inserted into the on-chain Merkle tree and becomes one of the members that later proofs will refer to.&lt;/p&gt;

&lt;p&gt;Before the deposit transaction is sent, the frontend first generates a credential locally and computes the corresponding &lt;code&gt;commitment&lt;/code&gt;. When the transaction goes on-chain, what actually enters the contract is the &lt;code&gt;commitment&lt;/code&gt; and the recipient address &lt;code&gt;B&lt;/code&gt;. The contract does not keep the credential for the user, and it does not write the raw secret on-chain. Later, if anyone wants to trigger release, they must first have that local credential.&lt;/p&gt;

&lt;p&gt;At release time, the browser uses that credential to do several things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compute the &lt;code&gt;commitment&lt;/code&gt; that belongs to this credential&lt;/li&gt;
&lt;li&gt;find its position in the Merkle tree locally&lt;/li&gt;
&lt;li&gt;generate a zk proof&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the proof is not sent straight into the contract yet.&lt;/p&gt;

&lt;p&gt;In this project, the proof first goes to the server API, then to Kurier, and then into zkVerify's verification path. Only after zkVerify returns a result that the chain can consume does the frontend continue.&lt;/p&gt;

&lt;p&gt;At the end of the flow, the frontend calls the contract's &lt;code&gt;finalize()&lt;/code&gt; with the aggregation result returned by zkVerify together with the &lt;code&gt;publicInputs&lt;/code&gt; for this proof.&lt;/p&gt;

&lt;p&gt;When the contract receives those parameters, it does not release the funds immediately. It first checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether this &lt;code&gt;commitment&lt;/code&gt; has a matching deposit in the contract&lt;/li&gt;
&lt;li&gt;whether this credential has already been used&lt;/li&gt;
&lt;li&gt;whether the statement behind the current proof is correct&lt;/li&gt;
&lt;li&gt;whether zkVerify's aggregation result can pass on-chain verification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after all of those checks pass does the contract send the original &lt;code&gt;0.1 ETH&lt;/code&gt; to the &lt;code&gt;B&lt;/code&gt; that was bound at deposit time, rather than to the person currently calling &lt;code&gt;finalize()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So, at a rough level, the full path can be remembered like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;A deposits 0.1 ETH -&amp;gt; B is locked in -&amp;gt; the frontend generates a local credential -&amp;gt; the browser produces a local proof -&amp;gt; the proof enters zkVerify's verification path -&amp;gt; the contract calls finalize -&amp;gt; 0.1 ETH is sent to B&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F47k52jqq4qq5anspsvx3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F47k52jqq4qq5anspsvx3.png" alt="zk escrow release flow diagram showing local credential creation, deposit with locked recipient B, on-chain state, browser proof generation, zkVerify validation path, and final release to B" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;commitment&lt;/code&gt; Is
&lt;/h3&gt;

&lt;p&gt;If you send the raw credential to the chain, the secret is gone. So the chain does not store &lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt; directly. It only stores the result derived from them, and that result is the &lt;code&gt;commitment&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One good way to think about &lt;code&gt;commitment&lt;/code&gt; is this:&lt;/p&gt;

&lt;p&gt;you put the raw credential into a sealed envelope. Other people cannot see what is inside, but they can see the unique mark on the outside. Later, that mark is what the system recognizes.&lt;/p&gt;

&lt;p&gt;When we say “you cannot reverse it back to the original content,” that does not mean it is mathematically impossible in an absolute sense. It means:&lt;/p&gt;

&lt;p&gt;if all you can see is the &lt;code&gt;commitment&lt;/code&gt;, it is very hard to recover the original &lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt;. That is exactly the job of the hash function here. It is easy to go from input to output, and hard to go from output back to input.&lt;/p&gt;

&lt;p&gt;This project uses &lt;code&gt;Pedersen hash&lt;/code&gt;. For now, you can think of it as a hash function designed to be friendly to ZK circuits. That is why it is common in zk projects and fits naturally into Circom-style constraints.&lt;/p&gt;

&lt;p&gt;In the frontend, the source of &lt;code&gt;pedersen&lt;/code&gt; is straightforward. It is loaded from &lt;code&gt;circomlibjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;circomlib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;circomlibjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;pedersenCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;circomlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buildPedersenHash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/escrow/prover.ts&lt;/code&gt; in &lt;code&gt;loadPedersen()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In this project, the frontend first generates &lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt; locally, then combines them and computes the &lt;code&gt;commitment&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitmentBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;commitmentBytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nullifierBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;commitmentBytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitmentPoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pedersen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commitmentBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitmentUnpacked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;babyJub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpackPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commitmentPoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;babyJub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commitmentUnpacked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/escrow/prover.ts&lt;/code&gt; in &lt;code&gt;computeCommitment()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once it is computed, what actually gets written into the contract is not the raw credential, but that &lt;code&gt;commitment&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function deposit(bytes32 commitment, address recipient) external payable {
    deposits[commitment] = DepositRecord({
        recipient: recipient,
        amount: msg.value,
        spent: false
    });

    uint32 leafIndex = _insert(commitment);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt; in &lt;code&gt;deposit()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So the role of &lt;code&gt;commitment&lt;/code&gt; is very direct: the frontend keeps the real secret, while the chain stores only a verifiable result that is hard to reverse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking at &lt;code&gt;nullifierHash&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;commitment&lt;/code&gt; alone is not enough.&lt;/p&gt;

&lt;p&gt;Because once someone gets the credential, they could in theory keep using it to trigger release again and again. The system still needs a way to know whether that credential has already been used.&lt;/p&gt;

&lt;p&gt;That is what &lt;code&gt;nullifierHash&lt;/code&gt; is for.&lt;/p&gt;

&lt;p&gt;You can think of it as a one-time redemption code.&lt;/p&gt;

&lt;p&gt;The most important point is this: the same credential always produces the same &lt;code&gt;nullifierHash&lt;/code&gt;. That is because it is derived only from &lt;code&gt;nullifier&lt;/code&gt;, and &lt;code&gt;nullifier&lt;/code&gt; is a fixed part of the credential.&lt;/p&gt;

&lt;p&gt;So the whole thing works a lot like a one-time ticket:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the first time, the system sees a redemption code it has never seen before, so it allows it&lt;/li&gt;
&lt;li&gt;after allowing it, the system records that code&lt;/li&gt;
&lt;li&gt;the second time, the same ticket produces the same code again&lt;/li&gt;
&lt;li&gt;the system checks its record, sees that the code was already used, and rejects it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means the system can detect “this is the same spend again, not a new valid spend” without exposing the raw credential itself.&lt;/p&gt;

&lt;p&gt;In the frontend, &lt;code&gt;nullifierHash&lt;/code&gt; is not computed from &lt;code&gt;nullifier + secret&lt;/code&gt;. It is computed from &lt;code&gt;nullifier&lt;/code&gt; alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierPoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pedersen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nullifierBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierUnpacked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;babyJub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpackPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nullifierPoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;babyJub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nullifierUnpacked&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/escrow/prover.ts&lt;/code&gt; in &lt;code&gt;computeCommitment()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In the contract, &lt;code&gt;finalize()&lt;/code&gt; reads &lt;code&gt;publicInputs[1]&lt;/code&gt; as &lt;code&gt;nullifierHash&lt;/code&gt;, then checks whether it has already been used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytes32 nullifierHash = bytes32(publicInputs[1]);
require(!nullifierUsed[nullifierHash], "nullifier used");

nullifierUsed[nullifierHash] = true;
dep.spent = true;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt; in &lt;code&gt;finalize()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The circuit also hard-binds this relationship: the private &lt;code&gt;nullifier&lt;/code&gt; that you submit must really derive the public &lt;code&gt;nullifierHash&lt;/code&gt;. That means you cannot swap in a fresh &lt;code&gt;nullifierHash&lt;/code&gt; and pretend it is a first-time use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hasher.nullifierHash === nullifierHash;
hasher.commitment === commitment;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/escrow/circom/escrowRelease.circom&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;nullifierHash&lt;/code&gt; is not solving generic “deduplication.” It solves a very specific problem:&lt;/p&gt;

&lt;p&gt;when the same credential comes back a second time, it exposes the same one-time redemption code, so the contract can stop it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finally, the Merkle Tree
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;commitment&lt;/code&gt; solves “you cannot store the raw secret on-chain,” and &lt;code&gt;nullifierHash&lt;/code&gt; solves “the same credential cannot be used twice,” but one more piece is still missing:&lt;/p&gt;

&lt;p&gt;how does the system know that the &lt;code&gt;commitment&lt;/code&gt; you are presenting really belongs to the set of valid on-chain deposits, rather than being something you just invented locally?&lt;/p&gt;

&lt;p&gt;That is what the Merkle tree is doing.&lt;/p&gt;

&lt;p&gt;You can think of it as a very long membership list. You do not have to bring the entire list with you. You only need to provide one path from “my item” up to the final summary, and the system can check whether you really are a member.&lt;/p&gt;

&lt;p&gt;In this project, every time the contract receives a new deposit, it inserts the corresponding &lt;code&gt;commitment&lt;/code&gt; into the tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint32 leafIndex = _insert(commitment);
emit MerkleRootUpdated(getLastRoot(), leafIndex);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt; in &lt;code&gt;deposit()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The tree maintenance logic itself lives in &lt;code&gt;MerkleTreeWithHistory.sol&lt;/code&gt;. After a new leaf is inserted, the root is recomputed upward. Later, &lt;code&gt;finalize()&lt;/code&gt; uses &lt;code&gt;isKnownRoot()&lt;/code&gt; to check whether the root provided by the proof is a root the contract has seen before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _insert(bytes32 _leaf) internal returns (uint32 index) { ... }

function isKnownRoot(bytes32 _root) public view returns (bool) { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/MerkleTreeWithHistory.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Before generating a proof locally, the frontend also computes the path for this leaf from the current commitment list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathElements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathIndices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildMerkleProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;leafIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/escrow/prover.ts&lt;/code&gt; in &lt;code&gt;buildMerkleProof()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Inside the circuit, the actual membership check looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tree.leaf &amp;lt;== hasher.commitment;
tree.root &amp;lt;== merkleRoot;
tree.pathElements[i] &amp;lt;== merklePath[i];
tree.pathIndices[i] &amp;lt;== merkleIndex[i];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/escrow/circom/escrowRelease.circom&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you translate those lines directly into plain English:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tree.leaf &amp;lt;== hasher.commitment;&lt;/code&gt;
tells the circuit that the leaf being checked is the &lt;code&gt;commitment&lt;/code&gt; derived from this credential&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tree.root &amp;lt;== merkleRoot;&lt;/code&gt;
tells the circuit that the target root is the public &lt;code&gt;merkleRoot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tree.pathElements[i] &amp;lt;== merklePath[i];&lt;/code&gt;
gives the circuit the sibling node for each level&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tree.pathIndices[i] &amp;lt;== merkleIndex[i];&lt;/code&gt;
tells the circuit whether the current node is on the left or on the right at each level&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Taken together, what the circuit is really doing is:&lt;/p&gt;

&lt;p&gt;starting from the &lt;code&gt;commitment&lt;/code&gt; leaf, recompute upward level by level following &lt;code&gt;merklePath&lt;/code&gt; and &lt;code&gt;merkleIndex&lt;/code&gt;. If the final result really equals the public &lt;code&gt;merkleRoot&lt;/code&gt;, then this &lt;code&gt;commitment&lt;/code&gt; really belongs to the tree.&lt;/p&gt;

&lt;p&gt;If the path is fake, or the leaf is not really in the tree, the recomputed root will not match, and the proof will fail.&lt;/p&gt;

&lt;p&gt;So the Merkle tree solves: how do you prove that this credential really belongs to one of the system's deposits?&lt;/p&gt;

&lt;p&gt;Put the three pieces together and their roles become clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;commitment&lt;/code&gt; turns the raw credential into something the chain can store&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nullifierHash&lt;/code&gt; makes sure the credential can only be used once&lt;/li&gt;
&lt;li&gt;the Merkle tree proves that the credential really belongs to the set of valid on-chain deposits&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What State the Contract Actually Stores On-Chain
&lt;/h2&gt;

&lt;p&gt;As explained earlier, this project is not “if the proof passes, then decide who gets paid.” Instead, the contract first records an escrow on-chain at deposit time, and the later proof only authorizes release.&lt;/p&gt;

&lt;p&gt;So this section focuses on one question: what exactly does the contract store on-chain?&lt;/p&gt;

&lt;h3&gt;
  
  
  The First Kind of State: the Data of Each Escrow
&lt;/h3&gt;

&lt;p&gt;The most important part is the &lt;code&gt;deposits&lt;/code&gt; mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mapping(bytes32 =&amp;gt; DepositRecord) public deposits;

struct DepositRecord {
    address recipient;
    uint256 amount;
    bool spent;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Its key is &lt;code&gt;commitment&lt;/code&gt;, and its value is the record for that escrow.&lt;/p&gt;

&lt;p&gt;Only three pieces of data are actually stored there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;recipient&lt;/code&gt;: who the funds are allowed to go to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;amount&lt;/code&gt;: how much money is locked&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spent&lt;/code&gt;: whether the escrow has already been released&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why this article has kept repeating that the recipient is locked at deposit time. The chain really stores it. Later, &lt;code&gt;finalize()&lt;/code&gt; does not decide the recipient again. It reads that existing record.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Second Kind of State: Which Commitments Belong to Valid Deposits
&lt;/h3&gt;

&lt;p&gt;Storing &lt;code&gt;deposits&lt;/code&gt; alone is not enough.&lt;/p&gt;

&lt;p&gt;Because the proof is not only proving “I know a credential.” It also needs to prove “the commitment behind this credential really belongs to one of the on-chain deposits.”&lt;/p&gt;

&lt;p&gt;So inside &lt;code&gt;deposit()&lt;/code&gt;, the contract does two things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deposits[commitment] = DepositRecord({
    recipient: recipient,
    amount: msg.value,
    spent: false
});

uint32 leafIndex = _insert(commitment);
emit MerkleRootUpdated(getLastRoot(), leafIndex);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt; in &lt;code&gt;deposit()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The first half stores the escrow record. The second half inserts the &lt;code&gt;commitment&lt;/code&gt; into the Merkle tree.&lt;/p&gt;

&lt;p&gt;The tree does not keep only the latest root. It also keeps a short history of roots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mapping(uint256 =&amp;gt; bytes32) public roots;
uint32 public constant ROOT_HISTORY_SIZE = 30;
uint32 public currentRootIndex = 0;
uint32 public nextIndex = 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/MerkleTreeWithHistory.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The reason is straightforward: when a user generates a proof locally, they may not happen to do it at the exact moment the latest root exists. As long as the proof's root still belongs to the contract's known root history, &lt;code&gt;finalize()&lt;/code&gt; can accept it.&lt;/p&gt;

&lt;p&gt;That is why &lt;code&gt;finalize()&lt;/code&gt; starts with this check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(isKnownRoot(merkleRoot), "root not known");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt; in &lt;code&gt;finalize()&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Third Kind of State: Which Credentials Have Already Been Consumed
&lt;/h3&gt;

&lt;p&gt;The same credential must not be used twice, so the contract also keeps a list of already-used credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mapping(bytes32 =&amp;gt; bool) public nullifierUsed;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;finalize()&lt;/code&gt;, the contract first checks whether this &lt;code&gt;nullifierHash&lt;/code&gt; has already appeared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(!nullifierUsed[nullifierHash], "nullifier used");

nullifierUsed[nullifierHash] = true;
dep.spent = true;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt; in &lt;code&gt;finalize()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Two things are updated here at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nullifierUsed[nullifierHash] = true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dep.spent = true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first prevents the same credential from being used again. The second marks the escrow itself as already released.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fourth Kind of State: Which Business Context This Proof Is Supposed to Belong To
&lt;/h3&gt;

&lt;p&gt;This project does not only validate “whether the proof is formally valid.” It also validates “whether the proof belongs to this business flow.”&lt;/p&gt;

&lt;p&gt;So the contract stores several expected configuration values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IZKVerifyAggregation public zkVerify;
bytes32 public vkHash;
uint256 public expectedDomain;
uint256 public expectedAppId;
uint256 public expectedChainId;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;These fields do different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;zkVerify&lt;/code&gt;: which aggregation verification contract the chain should call&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vkHash&lt;/code&gt;: which verification key this statement is tied to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expectedDomain / expectedAppId / expectedChainId&lt;/code&gt;: which business domain, application, and chain this proof is supposed to belong to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So before actually releasing the funds, &lt;code&gt;finalize()&lt;/code&gt; also checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(domain == expectedDomain, "domain mismatch");
require(appId == expectedAppId, "appId mismatch");
require(chainId == expectedChainId &amp;amp;&amp;amp; chainId == block.chainid, "chainId mismatch");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt; in &lt;code&gt;finalize()&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting Those Kinds of State Together
&lt;/h3&gt;

&lt;p&gt;If you think of the contract as a state container, it is really doing four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using &lt;code&gt;deposits&lt;/code&gt; to store the recipient, amount, and spent flag for each escrow&lt;/li&gt;
&lt;li&gt;using the Merkle tree and root history to store which &lt;code&gt;commitment&lt;/code&gt;s belong to valid deposits&lt;/li&gt;
&lt;li&gt;using &lt;code&gt;nullifierUsed&lt;/code&gt; to store which credentials have already been consumed&lt;/li&gt;
&lt;li&gt;using &lt;code&gt;zkVerify / vkHash / expectedDomain / expectedAppId / expectedChainId&lt;/code&gt; to store what the verification path is supposed to look like&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So later, &lt;code&gt;finalize()&lt;/code&gt; is not “release funds if there is a proof.” It compares the current proof against these on-chain states, one by one, and decides whether this is really a release the contract is willing to accept.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Circuit Actually Proves, and How the Browser Computes the Proof
&lt;/h2&gt;

&lt;p&gt;For many people, the moment they see the circuit, they immediately fall into details: which signals are public, which are private, which line does hashing, which line creates constraints.&lt;/p&gt;

&lt;p&gt;But if the overall structure is not clear first, those details quickly turn into a pile of symbols that are hard to follow.&lt;/p&gt;

&lt;p&gt;So start with the conclusion.&lt;/p&gt;

&lt;p&gt;This circuit is not proving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“I am the recipient &lt;code&gt;B&lt;/code&gt;”&lt;/li&gt;
&lt;li&gt;“I am the person who originally made the deposit”&lt;/li&gt;
&lt;li&gt;“I can now choose any address to receive the funds”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this circuit is really proving is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In plain language, the circuit is only proving two things:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;you really do hold the original credential&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;the &lt;code&gt;commitment&lt;/code&gt; derived from that credential really belongs to the on-chain Merkle tree&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those two facts hold, the system knows this is not a fake credential invented on the spot. It is a real credential tied to a real on-chain deposit.&lt;/p&gt;

&lt;p&gt;In other words, the circuit is only deciding one thing: whether this release request is backed by a valid credential that corresponds to an on-chain deposit. Who the funds are finally sent to is not decided here. That was already written into the contract record earlier, during deposit.&lt;/p&gt;

&lt;p&gt;That is also exactly what the comment in the circuit file says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Authorization‑only escrow release (no recipient in circuit)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/escrow/circom/escrowRelease.circom&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  First Look at the Two Kinds of Inputs in This Circuit
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;EscrowRelease(levels)&lt;/code&gt;, the inputs are explicitly split into public and private.&lt;/p&gt;

&lt;p&gt;The public inputs are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signal input merkleRoot;
signal input nullifierHash;
signal input commitment;
signal input domain;
signal input appId;
signal input chainId;
signal input timestamp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The private inputs are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signal input nullifier;
signal input secret;
signal input merklePath[levels];
signal input merkleIndex[levels];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/escrow/circom/escrowRelease.circom&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In plain language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;public inputs are values the outside world can see and verify against&lt;/li&gt;
&lt;li&gt;private inputs are values the prover knows but does not reveal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applied to this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt; are raw credential data, so they must be private&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;merklePath&lt;/code&gt; and &lt;code&gt;merkleIndex&lt;/code&gt; are used for membership proof, so they are also private&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commitment&lt;/code&gt;, &lt;code&gt;nullifierHash&lt;/code&gt;, and &lt;code&gt;merkleRoot&lt;/code&gt; are values the verifier must use, so they must be public&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;domain&lt;/code&gt;, &lt;code&gt;appId&lt;/code&gt;, &lt;code&gt;chainId&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt; are the business context for this proof, so they must also be public, otherwise the later statement and contract checks cannot bind the proof to the correct scenario&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Part One of the Circuit: Recompute the Public Results from the Private Credential
&lt;/h3&gt;

&lt;p&gt;The circuit does not start by checking the tree. It starts by recomputing:&lt;/p&gt;

&lt;p&gt;can the private &lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt; in this witness really derive the public &lt;code&gt;commitment&lt;/code&gt; and &lt;code&gt;nullifierHash&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;That logic lives in &lt;code&gt;CommitmentHasher()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;template CommitmentHasher() {
    signal input nullifier;
    signal input secret;
    signal output commitment;
    signal output nullifierHash;

    component commitmentHasher = Pedersen(496);
    component nullifierHasher = Pedersen(248);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/escrow/circom/escrowRelease.circom&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can split that logic into two steps.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt; are broken into bits.&lt;br&gt;&lt;br&gt;
Second, &lt;code&gt;commitmentHasher&lt;/code&gt; computes &lt;code&gt;commitment&lt;/code&gt; from &lt;code&gt;nullifier + secret&lt;/code&gt;, while &lt;code&gt;nullifierHasher&lt;/code&gt; computes &lt;code&gt;nullifierHash&lt;/code&gt; from &lt;code&gt;nullifier&lt;/code&gt; alone.&lt;/p&gt;

&lt;p&gt;These two lines are the key constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hasher.nullifierHash === nullifierHash;
hasher.commitment === commitment;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They do not mean “recompute it once and see what happens.” They mean:&lt;/p&gt;

&lt;p&gt;the circuit forces the private witness and the public inputs to match.&lt;/p&gt;

&lt;p&gt;If someone fills in a public &lt;code&gt;commitment&lt;/code&gt; arbitrarily, but the &lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt; they hold cannot really derive it, this part fails.&lt;br&gt;&lt;br&gt;
If someone tries to change &lt;code&gt;nullifierHash&lt;/code&gt; into a fresh, unused value to dodge replay protection, this part also fails, because it must really come from the same &lt;code&gt;nullifier&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Part Two of the Circuit: Check That This Commitment Really Belongs to the Tree
&lt;/h3&gt;

&lt;p&gt;The previous part proves only one thing: you know a raw credential that really derives the public &lt;code&gt;commitment&lt;/code&gt; and &lt;code&gt;nullifierHash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But one more step is still missing.&lt;/p&gt;

&lt;p&gt;The system still has to know that this &lt;code&gt;commitment&lt;/code&gt; is not something you just invented locally, but something that really belongs to the set of valid on-chain deposits.&lt;/p&gt;

&lt;p&gt;So the circuit next calls &lt;code&gt;MerkleTreeChecker(levels)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;component tree = MerkleTreeChecker(levels);
tree.leaf &amp;lt;== hasher.commitment;
tree.root &amp;lt;== merkleRoot;
for (var i = 0; i &amp;lt; levels; i++) {
    tree.pathElements[i] &amp;lt;== merklePath[i];
    tree.pathIndices[i] &amp;lt;== merkleIndex[i];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/escrow/circom/escrowRelease.circom&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The meaning is clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the leaf is the &lt;code&gt;commitment&lt;/code&gt; derived from this credential&lt;/li&gt;
&lt;li&gt;the target root is the public &lt;code&gt;merkleRoot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the prover must provide the path from that leaf up to the root&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;MerkleTreeChecker&lt;/code&gt; simply recomputes upward level by level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selectors[i].in[0] &amp;lt;== i == 0 ? leaf : hashers[i - 1].hash;
selectors[i].in[1] &amp;lt;== pathElements[i];
selectors[i].s &amp;lt;== pathIndices[i];

hashers[i].left &amp;lt;== selectors[i].out[0];
hashers[i].right &amp;lt;== selectors[i].out[1];
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/escrow/circom/merkleTree.circom&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;There are two key points here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pathElements[i]&lt;/code&gt; gives the sibling node at each level&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pathIndices[i]&lt;/code&gt; tells the circuit whether the current node is on the left or on the right&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the left-right order matters, the final hash changes when the order changes. So if any path element is fake, or any left-right order is wrong, the final root will not match.&lt;/p&gt;

&lt;p&gt;The last line in &lt;code&gt;MerkleTreeChecker&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root === hashers[levels - 1].hash;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its meaning is very direct: the root recomputed by the circuit must equal the public &lt;code&gt;merkleRoot&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part Three of the Circuit: Bind the Proof to the Current Business Context
&lt;/h3&gt;

&lt;p&gt;If the circuit only checked &lt;code&gt;commitment&lt;/code&gt; and Merkle membership, one piece would still be missing.&lt;/p&gt;

&lt;p&gt;Because the same membership proof could be reused elsewhere if there were no context binding.&lt;/p&gt;

&lt;p&gt;So this circuit also includes &lt;code&gt;domain&lt;/code&gt;, &lt;code&gt;appId&lt;/code&gt;, &lt;code&gt;chainId&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt; in the public inputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signal input domain;
signal input appId;
signal input chainId;
signal input timestamp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it forces those fields to participate in constraints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signal d2;
signal a2;
signal c2;
signal t2;
d2 &amp;lt;== domain * domain;
a2 &amp;lt;== appId * appId;
c2 &amp;lt;== chainId * chainId;
t2 &amp;lt;== timestamp * timestamp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;circuits/escrow/circom/escrowRelease.circom&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Those lines look simple, but they matter.&lt;/p&gt;

&lt;p&gt;The point is not to add a complicated new layer of business logic. The point is to make sure those public fields are actually used by the circuit, so they really become part of the statement behind this proof. Later, the contract checks them against &lt;code&gt;expectedDomain&lt;/code&gt;, &lt;code&gt;expectedAppId&lt;/code&gt;, and the current &lt;code&gt;chainId&lt;/code&gt;, and only then is the proof truly bound to this business flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  So What Is This Circuit Really Proving?
&lt;/h3&gt;

&lt;p&gt;Put the three parts together, and the circuit is really proving:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the prover knows &lt;code&gt;nullifier&lt;/code&gt; and &lt;code&gt;secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;those private values really derive the public &lt;code&gt;commitment&lt;/code&gt; and &lt;code&gt;nullifierHash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;that &lt;code&gt;commitment&lt;/code&gt; really belongs to the public &lt;code&gt;merkleRoot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the proof also carries the public context fields &lt;code&gt;domain / appId / chainId / timestamp&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So this is not a vague “I am allowed to release.”&lt;/p&gt;

&lt;p&gt;A more accurate sentence is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I know a valid credential, the commitment derived from it really belongs to an on-chain root, and this proof belongs to the current business context.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Browser Computes This Proof
&lt;/h3&gt;

&lt;p&gt;Once the circuit is clear, the order inside the frontend &lt;code&gt;proveEscrow()&lt;/code&gt; becomes much easier to follow.&lt;/p&gt;

&lt;p&gt;It does not call &lt;code&gt;snarkjs&lt;/code&gt; immediately. It first prepares every input the circuit needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nullifierHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;computeCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathElements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathIndices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildMerkleProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;leafIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/escrow/prover.ts&lt;/code&gt; in &lt;code&gt;proveEscrow()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Those three steps line up directly with the three things the circuit needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the raw private credential: &lt;code&gt;nullifier&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the public results derived from it: &lt;code&gt;commitment&lt;/code&gt;, &lt;code&gt;nullifierHash&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the membership path: &lt;code&gt;root&lt;/code&gt;, &lt;code&gt;pathElements&lt;/code&gt;, &lt;code&gt;pathIndices&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After that, the frontend packs those values into one circuit input object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;merkleRoot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;nullifierHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nullifierHash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pathElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="na"&gt;merkleIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pathIndices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/escrow/prover.ts&lt;/code&gt; in &lt;code&gt;proveEscrow()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Only then does it actually generate the proof:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicSignals&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;snarkjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groth16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullProve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;wasmPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;zkeyPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/zk/escrow/prover.ts&lt;/code&gt; in &lt;code&gt;proveEscrow()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So browser-side proving is not “throw some data into a library and magically get a proof.”&lt;br&gt;&lt;br&gt;
It is doing something very specific:&lt;/p&gt;

&lt;p&gt;prepare the full witness and public inputs that the circuit needs, then let &lt;code&gt;snarkjs&lt;/code&gt; generate a proof that an external verifier can check.&lt;/p&gt;

&lt;p&gt;At that point, the meaning of the proof is finally complete.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where zkVerify Enters the Flow After the Proof Is Generated
&lt;/h2&gt;

&lt;p&gt;By now, the contract and the circuit have each been explained on their own. The next step is to go back to the whole business flow, because that makes the later verification path much easier to understand. In a zk project, the circuit and the contract are only two layers. The full flow also includes proof generation, proof verification, and the way the verification result is finally consumed on-chain. So first look at the general zk flow, then map this project onto it.&lt;/p&gt;

&lt;p&gt;Before going further, it helps to place the overall flow in one frame.&lt;/p&gt;

&lt;p&gt;A zk project usually goes through two stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;first, generate a &lt;code&gt;proof&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;second, verify that &lt;code&gt;proof&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first stage happens on the prover side. The browser takes the inputs into the circuit, computes the witness, and calls the proving tool to generate a proof. That is where &lt;code&gt;wasm&lt;/code&gt; and &lt;code&gt;zkey&lt;/code&gt; are used. What is actually produced here is the new &lt;code&gt;proof&lt;/code&gt; for this user action.&lt;/p&gt;

&lt;p&gt;The second stage happens on the verifier side. The verification system takes that &lt;code&gt;proof&lt;/code&gt; together with the public inputs and checks whether it is valid. This stage uses the verification material tied to the circuit, namely &lt;code&gt;vk&lt;/code&gt;, or its identifier &lt;code&gt;vkHash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One timing detail matters here: &lt;code&gt;proof&lt;/code&gt; is generated fresh every time a user asks for a proof. &lt;code&gt;vk&lt;/code&gt; is not generated at that moment. It belongs to the earlier circuit preparation phase, where it is paired with &lt;code&gt;zkey&lt;/code&gt; for this circuit.&lt;/p&gt;

&lt;p&gt;Once you look at the timeline that way, the logic becomes much clearer.&lt;br&gt;
The first timeline is the “prepare the circuit” phase, which usually happens once.&lt;/p&gt;

&lt;p&gt;You first write the Circom circuit and compile it. After compilation, you get the &lt;code&gt;wasm&lt;/code&gt; and the corresponding constraint system. Then you run the Groth16 setup for that circuit, which gives you the proving material you need later: &lt;code&gt;zkey&lt;/code&gt; and the corresponding &lt;code&gt;vk&lt;/code&gt;.&lt;br&gt;
So &lt;code&gt;zkey&lt;/code&gt; and &lt;code&gt;vk&lt;/code&gt; do not appear out of nowhere. They both come from the same circuit, but they serve different roles: &lt;code&gt;zkey&lt;/code&gt; is for the prover to generate proofs, and &lt;code&gt;vk&lt;/code&gt; is for the verifier to check them.&lt;/p&gt;

&lt;p&gt;The second timeline is what happens every time a user actually asks for a proof.&lt;/p&gt;

&lt;p&gt;For example, when a user unlocks an escrow, the browser first prepares the inputs, then uses &lt;code&gt;wasm + zkey&lt;/code&gt; to generate a new &lt;code&gt;proof&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What is new every time is the &lt;code&gt;proof&lt;/code&gt;, not the &lt;code&gt;vk&lt;/code&gt;. The &lt;code&gt;vk&lt;/code&gt; was already prepared during the earlier setup phase, and it stays tied to this circuit.&lt;/p&gt;

&lt;p&gt;As for &lt;code&gt;vkHash&lt;/code&gt;, you can think of it as an identifier for the &lt;code&gt;vk&lt;/code&gt;.&lt;br&gt;
Many systems do not pass around the full &lt;code&gt;vk&lt;/code&gt; every time during runtime. Instead, they register the verification material up front and later use only an identifier such as &lt;code&gt;vkHash&lt;/code&gt;.&lt;br&gt;
This project works that way: the browser generates the &lt;code&gt;proof&lt;/code&gt;, while the later verification path is built around &lt;code&gt;proof&lt;/code&gt;, &lt;code&gt;publicInputs&lt;/code&gt;, and &lt;code&gt;vkHash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In other words, what is generated fresh for each user action in this project is the &lt;code&gt;proof&lt;/code&gt;, while &lt;code&gt;vk&lt;/code&gt; / &lt;code&gt;vkHash&lt;/code&gt; points to the verification material that was already prepared for this circuit.&lt;/p&gt;

&lt;p&gt;In this project, the browser is responsible for generating the proof;&lt;br&gt;
the later verification path goes through Kurier and zkVerify;&lt;br&gt;
and what the chain finally consumes is not simply “the raw proof that just came out of the browser,” but the verification result returned by zkVerify.&lt;/p&gt;

&lt;p&gt;At this point, the browser can already do something powerful:&lt;/p&gt;

&lt;p&gt;it can generate a proof locally that says “I know a valid credential, and it belongs to some on-chain root.”&lt;/p&gt;

&lt;p&gt;But that is still not the end of the flow.&lt;/p&gt;

&lt;p&gt;Because what the business logic needs is not “a proof appeared in the browser.” It needs “the contract is willing to execute a real release based on that proof.”&lt;/p&gt;

&lt;p&gt;There is still one verification layer between those two things.&lt;/p&gt;

&lt;p&gt;That is exactly the layer zkVerify fills in this system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk413n7r8nnt432e6uxp3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk413n7r8nnt432e6uxp3.png" alt="zkVerify consumption flow diagram showing circuit setup, browser proof generation, server API submission, proof status tracking, aggregation result retrieval, gateway verification, and escrow finalize on the target chain" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Generating the Proof Is Still Not Enough
&lt;/h3&gt;

&lt;p&gt;If the system used the route where the contract verifies the Groth16 proof directly, then once the browser generated the proof, the next step would simply be to send that proof into a verifier contract.&lt;/p&gt;

&lt;p&gt;But this project does not work like that.&lt;/p&gt;

&lt;p&gt;The route here is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the browser first generates the proof locally&lt;/li&gt;
&lt;li&gt;the server forwards the proof to Kurier&lt;/li&gt;
&lt;li&gt;Kurier and zkVerify process that proof&lt;/li&gt;
&lt;li&gt;the frontend receives an aggregation result that the chain can consume&lt;/li&gt;
&lt;li&gt;finally, the contract uses &lt;code&gt;verifyProofAggregation(...)&lt;/code&gt; to decide whether &lt;code&gt;finalize()&lt;/code&gt; can pass&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So zkVerify here is not the prover, and it is not the place where the credential is generated.&lt;br&gt;&lt;br&gt;
Its job is this: &lt;strong&gt;take an already-generated proof and turn it into a verification result that an on-chain contract can rely on.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What &lt;code&gt;/api/submit-proof&lt;/code&gt; Does
&lt;/h3&gt;

&lt;p&gt;Once the proof comes out of the browser, the first stop is not the contract. It is the server-side &lt;code&gt;/api/submit-proof&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This layer has two jobs.&lt;/p&gt;

&lt;p&gt;First, it keeps the Kurier API key on the server rather than exposing it to the frontend.&lt;br&gt;&lt;br&gt;
Second, before forwarding the proof, it cross-checks several key fields so that the frontend does not send out a proof whose context is inconsistent.&lt;/p&gt;

&lt;p&gt;The most important checks are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chainFromInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chainFromInputs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;chainFromInputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domainFromInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domainFromInputs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;domainFromInputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appIdFromInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appIdFromInputs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;appIdFromInputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nullifierFromInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nullifierFromInputs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;antiReplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;nullifierFromInputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/api/submit-proof.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In other words, the server is not blindly forwarding the proof. It first confirms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the request &lt;code&gt;chainId&lt;/code&gt; and the proof's &lt;code&gt;publicInputs[5]&lt;/code&gt; are the same value&lt;/li&gt;
&lt;li&gt;the request &lt;code&gt;domain&lt;/code&gt; and &lt;code&gt;appId&lt;/code&gt; really match the proof's public inputs&lt;/li&gt;
&lt;li&gt;the anti-replay &lt;code&gt;nullifier&lt;/code&gt; also matches the proof's &lt;code&gt;nullifierHash&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after those checks pass does the server build the Kurier submission payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;proofType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;groth16&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;vkRegistered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;proofOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getProofOptions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;proofData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;publicSignals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;vk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vkHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/api/submit-proof.ts&lt;/code&gt; in &lt;code&gt;buildKurierSubmitPayload()&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;/api/proof-status&lt;/code&gt; Does
&lt;/h3&gt;

&lt;p&gt;After the proof is submitted, the frontend needs to know where it is in the pipeline.&lt;/p&gt;

&lt;p&gt;That is the job of &lt;code&gt;/api/proof-status&lt;/code&gt;. It queries Kurier for the job status, then normalizes several possible raw fields into a frontend-friendly status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusRaw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobStatus&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;jobStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeKurierStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusRaw&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;rawStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;statusRaw&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;aggregationDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/api/proof-status.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;One point matters here.&lt;/p&gt;

&lt;p&gt;The Kurier status string is useful, but it is not the final condition that decides whether &lt;code&gt;finalize()&lt;/code&gt; can pass on-chain. It is mostly telling the frontend where the proof is in the pipeline.&lt;/p&gt;

&lt;p&gt;What really determines whether the contract can consume the result is whether the correct aggregation tuple can be retrieved later, and whether that tuple can pass the on-chain &lt;code&gt;verifyProofAggregation(...)&lt;/code&gt; check.&lt;/p&gt;

&lt;h3&gt;
  
  
  What &lt;code&gt;/api/proof-aggregation&lt;/code&gt; Does
&lt;/h3&gt;

&lt;p&gt;When the proof reaches the &lt;code&gt;aggregated&lt;/code&gt; status, the frontend requests &lt;code&gt;/api/proof-aggregation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This layer is not verifying the proof again. It is extracting the exact fields from Kurier's response that the contract actually needs later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;domainId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aggregationId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leafCount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;merklePath&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leaf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/api/proof-aggregation.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The most important point here is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the &lt;code&gt;domainId&lt;/code&gt; here is not the same thing as the circuit public input &lt;code&gt;domain&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code explicitly says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// zkVerify domainId (aggregation domain) is NOT the same semantic field as&lt;/span&gt;
&lt;span class="c1"&gt;// circuit public input domain/app domain. Never fallback to DOMAIN=1 here.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/api/proof-aggregation.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Those two concepts have completely different roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;domain&lt;/code&gt; in the circuit is part of the business context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;domainId&lt;/code&gt; here is the zkVerify aggregation domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those two ideas get mixed up, &lt;code&gt;finalize()&lt;/code&gt; is very likely to fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  What zkVerify Ultimately Provides
&lt;/h3&gt;

&lt;p&gt;By this point, what zkVerify gives this flow is not an abstract “verified” badge. It gives a set of data that the contract can actually consume.&lt;/p&gt;

&lt;p&gt;From the frontend's point of view, the two most important outputs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the aggregation tuple: &lt;code&gt;domainId / aggregationId / leafCount / index / merklePath&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the corresponding &lt;code&gt;leaf&lt;/code&gt; in the aggregation tree&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the frontend has those two things, it can first compare the statement locally, then let the contract call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zkVerify.verifyProofAggregation(
    domainId,
    aggregationId,
    leaf,
    merklePath,
    leafCount,
    index
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;contracts/src/ZKEscrowRelease.sol&lt;/code&gt; in &lt;code&gt;finalize()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So if you had to summarize zkVerify's role here in one sentence, it would be:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the browser generates the proof, and zkVerify turns that proof into a verification result that the contract can consume.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Frontend Uses the zkVerify Result on the Target Chain
&lt;/h2&gt;

&lt;p&gt;Now the previous sections connect into one flow.&lt;/p&gt;

&lt;p&gt;Look at it in the real order.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Start from the Credential and Produce the Local Proof
&lt;/h3&gt;

&lt;p&gt;When the unlock flow starts, the frontend reads the user-provided credential, fetches the on-chain commitments, and finds the leaf that corresponds to that credential:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchCommitments&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrawCredential&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;commitment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;computeCommitment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leafIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commitments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/escrow.tsx&lt;/code&gt; in &lt;code&gt;handleUnlock()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once it finds the leaf, the frontend calls &lt;code&gt;proveEscrow()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;proveEscrow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;withdrawCredential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;leaves&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commitments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;leafIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeChainId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/escrow.tsx&lt;/code&gt; in &lt;code&gt;handleUnlock()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;At that point, the browser already holds the local proof and the &lt;code&gt;publicInputs&lt;/code&gt; for that proof.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Run Local Prechecks First
&lt;/h3&gt;

&lt;p&gt;After the proof is generated, the frontend still does not submit it immediately.&lt;/p&gt;

&lt;p&gt;It first checks two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether the proof's &lt;code&gt;merkleRoot&lt;/code&gt; is a root the contract recognizes&lt;/li&gt;
&lt;li&gt;whether the proof's &lt;code&gt;chainId&lt;/code&gt; matches the chain the wallet is currently connected to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The corresponding code is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;publicClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readContract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;escrowAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;abi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;escrowAbi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;isKnownRoot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;proofRoot&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;proofChainId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;activeChainId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`chainId mismatch (wallet=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;activeChainId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, proof=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;proofChainId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/escrow.tsx&lt;/code&gt; in &lt;code&gt;handleUnlock()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The purpose is simple: stop proofs that are obviously impossible before they go any further.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Submit the Proof, Then Poll for Status
&lt;/h3&gt;

&lt;p&gt;After the prechecks pass, the frontend calls &lt;code&gt;/api/submit-proof&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/submit-proof&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`proof_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domainId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;userAddr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;chainId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;proofChainId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;antiReplay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;nullifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nullifierHash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/escrow.tsx&lt;/code&gt; in &lt;code&gt;handleUnlock()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once submission succeeds, the frontend keeps calling &lt;code&gt;/api/proof-status&lt;/code&gt; until the status becomes &lt;code&gt;aggregated&lt;/code&gt;. That status does not mean the business flow is finished. It means the frontend can now fetch the aggregation tuple.&lt;/p&gt;

&lt;p&gt;One more time: &lt;code&gt;aggregated&lt;/code&gt; is not the end of the flow. It only means zkVerify's side has finished preparing the verification result. The step that actually makes the business logic happen has not happened yet. The frontend still needs to send a &lt;code&gt;finalize()&lt;/code&gt; transaction to the escrow contract on the target chain. Only when that on-chain call succeeds is the release really complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Fetch the Aggregation Tuple and Confirm It Really Belongs to This Proof
&lt;/h3&gt;

&lt;p&gt;Once the status becomes &lt;code&gt;aggregated&lt;/code&gt;, the frontend calls &lt;code&gt;/api/proof-aggregation&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aggRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/proof-aggregation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;submitData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/escrow.tsx&lt;/code&gt; in &lt;code&gt;handleUnlock()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What comes back is not “another proof.” It is the aggregation tuple that the later on-chain verification will use.&lt;/p&gt;

&lt;p&gt;But the frontend also performs one very important comparison:&lt;br&gt;&lt;br&gt;
it reads the on-chain &lt;code&gt;vkHash&lt;/code&gt;, recomputes the statement locally from the current &lt;code&gt;publicInputs&lt;/code&gt;, and compares that to the &lt;code&gt;leaf&lt;/code&gt; returned from the aggregation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onChainVkHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;publicClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readContract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;escrowAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;abi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;escrowAbi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vkHash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localStatement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computeLocalStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChainVkHash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStatement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;aggLeaf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`Proof/job mismatch (statement=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;localStatement&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, leaf=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;aggLeaf&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;).`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/escrow.tsx&lt;/code&gt; in &lt;code&gt;handleUnlock()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What this step is asking is:&lt;/p&gt;

&lt;p&gt;is the &lt;code&gt;leaf&lt;/code&gt; returned by zkVerify really the statement produced by this exact proof?&lt;/p&gt;

&lt;p&gt;If that does not match, then even with the tuple in hand, the later contract call will not pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Run the zkVerify Precheck, Then Call &lt;code&gt;finalize()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;After the statement matches, the frontend still does not send a transaction immediately.&lt;/p&gt;

&lt;p&gt;It first uses that tuple to call the zkVerify contract through a read-only precheck. The &lt;code&gt;zkVerifyAddr&lt;/code&gt; used here is not just any address. It is the official zkVerify gateway/proxy address configured in the escrow contract:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;zkVerifyAddr&lt;/code&gt; read here is not just some hardcoded contract address inside the project. It is the official zkVerify gateway/proxy address provided on the target chain. For the Base Sepolia setup used in this tutorial, the official address is &lt;a href="https://sepolia.basescan.org/address/0x0807C544D38aE7729f8798388d89Be6502A1e8A8" rel="noopener noreferrer"&gt;&lt;code&gt;0x0807C544D38aE7729f8798388d89Be6502A1e8A8&lt;/code&gt;&lt;/a&gt;. For the full list of addresses, see the zkVerify docs: &lt;a href="https://docs.zkverify.io/architecture/contract-addresses" rel="noopener noreferrer"&gt;Contract Addresses&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;zkvOk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;publicClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readContract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;zkVerifyAddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;abi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;zkVerifyAbi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifyProofAggregation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;domainIdFromAgg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aggregationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;localStatement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/escrow.tsx&lt;/code&gt; in &lt;code&gt;handleUnlock()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This call is an &lt;code&gt;eth_call&lt;/code&gt;. Its purpose is to ask one question first: if the frontend now really sends &lt;code&gt;finalize()&lt;/code&gt; to the target chain, will the official zkVerify gateway/proxy contract accept this aggregation check?&lt;/p&gt;

&lt;p&gt;Only after that precheck passes does the frontend simulate &lt;code&gt;finalize()&lt;/code&gt;, then actually send the transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;publicClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;simulateContract&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;escrowAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;abi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;escrowAbi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finalize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;domainIdFromAgg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;aggregationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;leafCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;txHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeContractAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;escrowAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;abi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;escrowAbi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finalize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;domainIdFromAgg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aggregationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;merklePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leafCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicInputs&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code location: &lt;code&gt;apps/web/src/pages/escrow.tsx&lt;/code&gt; in &lt;code&gt;handleUnlock()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In other words, the real allow path is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the local proof is generated&lt;/li&gt;
&lt;li&gt;the Kurier job reaches &lt;code&gt;aggregated&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the aggregation tuple is fetched successfully&lt;/li&gt;
&lt;li&gt;the local statement matches the aggregation &lt;code&gt;leaf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;verifyProofAggregation(...)&lt;/code&gt; precheck passes&lt;/li&gt;
&lt;li&gt;only then is &lt;code&gt;finalize()&lt;/code&gt; actually sent on-chain&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One more thing needs to be said here: the read-only precheck does not mean the business flow is complete. Once &lt;code&gt;finalize()&lt;/code&gt; is really sent, the escrow contract on the target chain will internally call the official zkVerify gateway/proxy contract's &lt;code&gt;verifyProofAggregation(...)&lt;/code&gt; again. Only if that on-chain call also passes does &lt;code&gt;finalize()&lt;/code&gt; continue to release the funds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Summary
&lt;/h3&gt;

&lt;p&gt;At this point, the frontend has already prepared everything it can.&lt;/p&gt;

&lt;p&gt;The parameters received by &lt;code&gt;finalize()&lt;/code&gt; are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;domainIdFromAgg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aggregationId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;merklePath&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leafCount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;publicInputs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the contract runs one final round of checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether &lt;code&gt;publicInputs&lt;/code&gt; has the correct length&lt;/li&gt;
&lt;li&gt;whether &lt;code&gt;merkleRoot&lt;/code&gt; is a known root&lt;/li&gt;
&lt;li&gt;whether &lt;code&gt;domain / appId / chainId / timestamp&lt;/code&gt; are correct&lt;/li&gt;
&lt;li&gt;whether &lt;code&gt;verifyProofAggregation(...)&lt;/code&gt; passes&lt;/li&gt;
&lt;li&gt;whether &lt;code&gt;nullifierHash&lt;/code&gt; has already been used&lt;/li&gt;
&lt;li&gt;whether &lt;code&gt;deposits[commitment]&lt;/code&gt; exists and is still not &lt;code&gt;spent&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after all of those checks pass does the contract actually send the funds to the &lt;code&gt;recipient&lt;/code&gt; that was locked at deposit time.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>beginners</category>
      <category>blockchain</category>
      <category>zk</category>
    </item>
  </channel>
</rss>
