<?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>Forem: zk_nd3r</title>
    <description>The latest articles on Forem by zk_nd3r (@zknd3r).</description>
    <link>https://forem.com/zknd3r</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%2F3866560%2F4b858a42-881c-4a24-911a-8a976060996c.jpeg</url>
      <title>Forem: zk_nd3r</title>
      <link>https://forem.com/zknd3r</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/zknd3r"/>
    <language>en</language>
    <item>
      <title>Building a Crosslink feature net explorer in 90 minutes (FastAPI + nginx + certbot)</title>
      <dc:creator>zk_nd3r</dc:creator>
      <pubDate>Thu, 16 Apr 2026 00:59:34 +0000</pubDate>
      <link>https://forem.com/zknd3r/building-a-crosslink-feature-net-explorer-in-90-minutes-fastapi-nginx-certbot-1jgk</link>
      <guid>https://forem.com/zknd3r/building-a-crosslink-feature-net-explorer-in-90-minutes-fastapi-nginx-certbot-1jgk</guid>
      <description>&lt;p&gt;someone asked for a block explorer in a workshop signal group on a tuesday afternoon. ninety minutes later one was live at &lt;a href="https://ctaz.frontiercompute.cash" rel="noopener noreferrer"&gt;ctaz.frontiercompute.cash&lt;/a&gt;, source MIT at &lt;a href="https://github.com/Frontier-Compute/ctaz-explorer" rel="noopener noreferrer"&gt;github.com/Frontier-Compute/ctaz-explorer&lt;/a&gt;. this is a writeup of what it took and which choices cost time.&lt;/p&gt;

&lt;p&gt;the tldr: about five hundred lines of python and jinja, no build step, no javascript framework, no analytics, no cookies, eighteen routes, server-side svg charts, systemd unit, nginx reverse proxy, lets encrypt cert. the hardest part was a port collision.&lt;/p&gt;




&lt;h2&gt;
  
  
  the chain
&lt;/h2&gt;

&lt;p&gt;zcash crosslink is a hybrid consensus design. proof of work blocks are produced as usual, and a bft finality gadget stamps them finalized after a delay. it is currently running as an incentivized feature net called ctaz, which is short for crosslink testnet. the protocol page is at &lt;a href="https://shieldedlabs.net/crosslink" rel="noopener noreferrer"&gt;shieldedlabs.net/crosslink&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;the practical consequence for an explorer is that every block has two states that matter, not one. "this block exists on the chain" and "this block has been finalized by the bft layer" are different questions with different answers. so every surface that shows a block needs to answer both.&lt;/p&gt;

&lt;p&gt;there was no public explorer for the ctaz feature net at the time of writing. the protocol had a running node, a json rpc, and a workshop full of engineers staring at raw hex.&lt;/p&gt;




&lt;h2&gt;
  
  
  stack choices
&lt;/h2&gt;

&lt;p&gt;the choices took about four minutes of deliberation, because the constraint was time and also, do not build anything that needs a sidecar service to render a page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;fastapi + jinja2.&lt;/strong&gt; python is what the operator already had installed and warm. fastapi gives typed request handling and automatic openapi. jinja2 renders strings on the server. a react or svelte client would have required a build step, a bundler, and a deploy pipeline. on a ninety minute clock those are cost centers, not features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nginx + certbot.&lt;/strong&gt; cloudflare in front of a site gives tls and a cdn, and also gives cloudflare a view into every request and every ip. nginx with lets encrypt gives tls and nothing else, which is the correct amount of third party for a tool whose purpose is to let people verify things themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;systemd unit.&lt;/strong&gt; a single python process with a virtualenv did not need a container runtime. the systemd unit is sixteen lines, the environment variable for the rpc port lives inline, and &lt;code&gt;systemctl status ctaz-explorer&lt;/code&gt; is shorter to type than the docker equivalent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;no database.&lt;/strong&gt; every page is rendered from a live rpc call. the rpc node is on the same box, so latency is microseconds. premature persistence is how weekend projects become monorepos.&lt;/p&gt;




&lt;h2&gt;
  
  
  the rpc dance
&lt;/h2&gt;

&lt;p&gt;this part was not ninety minutes of clean flow. the rpc stack needed debugging.&lt;/p&gt;

&lt;p&gt;zebra-crosslink, the monolith fork that runs the bft layer, builds from commit &lt;code&gt;d880b65&lt;/code&gt; tagged "speculative build" by sam smith. it compiles, it syncs, it exposes the standard zebra json rpc surface. the default rpc port is 8232.&lt;/p&gt;

&lt;p&gt;the default rpc port was already taken. an unrelated docker zebra-mainnet on the same vps had claimed 8232 months earlier. so the ctaz explorer runs its backing zebra-crosslink on 18232 via an env var override.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/ctaz-explorer.service
&lt;/span&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ZEBRAD_RPC_URL=http://127.0.0.1:18232&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/ctaz-explorer/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 8088&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ZEBRAD_RPC_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ZEBRAD_RPC_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8232&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ZEBRAD_RPC_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jsonrpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;or&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="n"&gt;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;thirty seconds of code, about twenty minutes to figure out why &lt;code&gt;getblockcount&lt;/code&gt; was returning the wrong chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  finality badges
&lt;/h2&gt;

&lt;p&gt;zebra-crosslink exposes a method called &lt;code&gt;get_tfl_block_finality_from_hash&lt;/code&gt;. tfl stands for trailing finality layer. given a block hash it returns one of "Finalized", "NotYetFinalized", or an error. that is almost the entire ux model for a hybrid pow plus bft chain in three string values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;finality_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&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;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_tfl_block_finality_from_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;block_hash&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;three css classes, three colors. gold for finalized, blue for seen but not yet finalized, gray for unknown.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.badge-final&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#d4a017&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.badge-pending&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2b6cb0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.badge-unknown&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4a5568&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;block.finality&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Finalized"&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge badge-final"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;finalized&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="nv"&gt;elif&lt;/span&gt; &lt;span class="nv"&gt;block.finality&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"NotYetFinalized"&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge badge-pending"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;not yet finalized&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"badge badge-unknown"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;unknown&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endif&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the badge shows up inline on the homepage block list, on the block detail page, and on every tx page since a tx inherits its block's finality. an honest detail: at the time of writing the finality gap on the ctaz feature net is about eighteen hundred blocks. the explorer displays that accurately. a chain does not need to be perfect for its explorer to be honest about where it is.&lt;/p&gt;




&lt;h2&gt;
  
  
  pool deltas
&lt;/h2&gt;

&lt;p&gt;every zcash block returned by &lt;code&gt;getblock&lt;/code&gt; verbose includes a &lt;code&gt;valuePools&lt;/code&gt; array. one entry per shielded pool, with a &lt;code&gt;valueDelta&lt;/code&gt; that tells you how much zec moved in or out of that pool in that block. the pools are transparent, sprout, sapling, orchard, and on crosslink there is also a lockbox pool.&lt;/p&gt;

&lt;p&gt;no explorer on ctaz was showing per block pool flow. that was a gap worth filling, and the data was already there in the rpc response. so the explorer computes a two hundred block rolling window and renders one svg sparkline per pool, cumulatively, with no javascript involved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sparkline_svg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="n"&gt;pts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&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="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;
        &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;
        &lt;span class="n"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;svg viewBox=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 0 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; xmlns=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://www.w3.org/2000/svg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;polyline fill=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; stroke=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currentColor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; stroke-width=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;points=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the polyline string goes straight into the jinja template with the &lt;code&gt;|safe&lt;/code&gt; filter. browser renders svg natively. no chart library, no hydration, no skeleton loader. the page ships in one request.&lt;/p&gt;

&lt;p&gt;/pool/orchard, /pool/transparent, /pool/sapling, /pool/sprout, /pool/lockbox. each has a matching /api/pool/{id} that returns the raw series as json for anyone who wants to pipe it into their own tooling.&lt;/p&gt;




&lt;h2&gt;
  
  
  attestation registries
&lt;/h2&gt;

&lt;p&gt;three small json files in the repo hold a registry for each of three protocols that sit on top of zcash: zap1 anchors, shieldedvault custody events, and zeven event streams. the explorer loads them at request time and matches them against the txid being viewed. if there is a match, a typed card renders on the tx page explaining which registry matched and what claim is being made.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;REGISTRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/anchors.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vaults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/vaults.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/events.json&lt;/span&gt;&lt;span class="sh"&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;def&lt;/span&gt; &lt;span class="nf"&gt;registry_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;REGISTRIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;on the ctaz feature net these registries are currently empty, because no anchors or custody events have been published to this chain yet. the /anchors, /vaults, and /events pages say so honestly, with a one line explanation of what will land there when the feature net gets real traffic. fake demo content would have been faster to render and would have been a lie. empty state with a sentence of context costs nothing and keeps the explorer trustworthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  /verify
&lt;/h2&gt;

&lt;p&gt;one page, one input field, one purpose. paste a txid. get back a json object containing the block, the finality status, the pool deltas affected, and any registry matches. one request, one answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/verify/{txid}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&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="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;getrawtransaction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;txid&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="n"&gt;block&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;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;getblock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blockhash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;txid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;txid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block_height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finality&lt;/span&gt;&lt;span class="sh"&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;finality_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value_pools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valuePools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registry_matches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;registry_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txid&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;no auth, no api key, no rate limit, curl friendly. the whole point of the tool is that every claim the explorer makes elsewhere is independently verifiable by anyone running their own crosslink node, and /verify is just the shortcut.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://ctaz.frontiercompute.cash/api/verify/&amp;lt;txid&amp;gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  /why
&lt;/h2&gt;

&lt;p&gt;a link at the top of the homepage labeled "why this exists" goes to a page explaining the design choices above in more detail, with every claim anchored to a file and line in the repo. a block explorer explaining why it renders its own pages is recursively silly, but it answers the most common question a first time visitor has.&lt;/p&gt;




&lt;h2&gt;
  
  
  what does not work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;the finality gap is large. the explorer shows it accurately but does not speed it up.&lt;/li&gt;
&lt;li&gt;the 200 block sparkline window is a constant in source, not configurable per request.&lt;/li&gt;
&lt;li&gt;no websocket push for new blocks, the homepage refreshes on page load only.&lt;/li&gt;
&lt;li&gt;no search across addresses yet, only direct address page lookup by string match.&lt;/li&gt;
&lt;li&gt;on rpc failure the page returns a 503 json blob, not a friendly error page with a retry hint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;none of those are hard. they are just not ninety minutes of work.&lt;/p&gt;




&lt;h2&gt;
  
  
  closing
&lt;/h2&gt;

&lt;p&gt;this is not a product. there is no signup, no cta, no dashboard, no email capture. source is mit at &lt;a href="https://github.com/Frontier-Compute/ctaz-explorer" rel="noopener noreferrer"&gt;github.com/Frontier-Compute/ctaz-explorer&lt;/a&gt;. if something looks wrong, open an issue. if something looks right, just use it.&lt;/p&gt;

&lt;p&gt;the full route list, for reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/                   home, five panel dashboard
/block/{x}          block detail with finality badge and pool deltas
/tx/{x}             tx detail with attestation cards if any match
/address/{x}        address page
/finalizers         bft finalizer set
/stake              stake distribution
/params             protocol params
/verify             one shot proof tool
/why                design notes
/pool/{id}          per pool cumulative sparkline page (5 pools)
/anchors            zap1 anchor registry
/vaults             shieldedvault registry
/events             zeven event stream
/api/*              json versions of all of the above
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;eighteen routes, about five hundred lines of python and jinja, one afternoon. the ninety minutes is real but the ninety minutes is also the result of every choice above being the cheap one. react would have been a day. a database would have been a day. cloudflare in front would have been an hour of dns plus a permanent third party. none of those buy anything a feature net explorer needs on day one.&lt;/p&gt;

&lt;p&gt;if the next person wants to fork it and point it at a different chain, the rpc url is an env var and the templates are plain jinja. that is the whole porting guide.&lt;/p&gt;

</description>
      <category>zcash</category>
      <category>fastapi</category>
      <category>webdev</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>5 properties of agent custody</title>
      <dc:creator>zk_nd3r</dc:creator>
      <pubDate>Thu, 09 Apr 2026 03:28:52 +0000</pubDate>
      <link>https://forem.com/zknd3r/verifying-zcash-proofs-on-ethereum-with-eip-152-1c9g</link>
      <guid>https://forem.com/zknd3r/verifying-zcash-proofs-on-ethereum-with-eip-152-1c9g</guid>
      <description>&lt;p&gt;&lt;a href="https://frontiercompute.cash" rel="noopener noreferrer"&gt;&lt;/a&gt;  your AI agent can write code, book flights, manage a calendar.  now hand it a wallet.&lt;/p&gt;

&lt;p&gt;suddenly we are back to "trust me bro" with a hot key.&lt;/p&gt;

&lt;p&gt;agent frameworks are everywhere.  agent custody is nowhere.  the gap between what agents can do and what agents can safely hold is the most dangerous problem in AI infrastructure right now.&lt;/p&gt;

&lt;p&gt;here are five properties every agent wallet needs.  if your agent is missing any of them, you do not have custody.  you have a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. non-drainable
&lt;/h2&gt;

&lt;p&gt;no single key compromise should drain the wallet.&lt;/p&gt;

&lt;p&gt;most agent wallets today give the agent a private key.  or they custody it on a server.  either way, one compromise and the funds are gone.&lt;/p&gt;

&lt;p&gt;the fix is threshold signing.  split the key into shares.  require 2 of 3 (or 3 of 5, or any t of n) to produce a valid signature.  the agent holds one share.  a policy service holds another.  a recovery key sits in cold storage.&lt;/p&gt;

&lt;p&gt;FROST is one way to do this.  2PC-MPC is another.  the mechanism matters less than the property: no single point of failure drains funds.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. policy-bound
&lt;/h2&gt;

&lt;p&gt;spend limits, allowlists, and time locks must be enforced on chain.  not in client code.&lt;/p&gt;

&lt;p&gt;if your spending policy runs on the same server as your agent, the agent can modify the policy.  or the server can be compromised and the policy disabled.&lt;/p&gt;

&lt;p&gt;on-chain policy means the rules exist in a smart contract.  the agent can request a transaction but the chain enforces the limits.  the agent cannot override what the chain does not permit.&lt;/p&gt;

&lt;p&gt;Sui Move is one approach.  EVM modifiers are another.  the key property: policy enforcement is independent of the agent runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. attested
&lt;/h2&gt;

&lt;p&gt;every action the agent takes should produce a verifiable proof.&lt;/p&gt;

&lt;p&gt;when your agent moves money, there should be a cryptographic commitment that says: this agent, at this time, took this action, with this result.  anchored to a public ledger.  independently verifiable.&lt;/p&gt;

&lt;p&gt;this is not logging.  logs are controlled by the operator.  attestation is committed to a chain that the operator cannot alter after the fact.&lt;/p&gt;

&lt;p&gt;ZAP1 is one implementation.  any Merkle tree anchored to an immutable ledger achieves the property.  the key: proof exists outside the system being proven.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. remembering
&lt;/h2&gt;

&lt;p&gt;the agent should carry context across sessions.&lt;/p&gt;

&lt;p&gt;an agent that forgets what it did last session is an agent that repeats mistakes.  custody requires memory.  what positions are open.  what policies were active.  what decisions led to the current state.&lt;/p&gt;

&lt;p&gt;this is not just a database.  it is a knowledge graph that the agent can query semantically.  "what happened last time volatility spiked" should return an answer, not a blank stare.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. killable
&lt;/h2&gt;

&lt;p&gt;a human or risk system must be able to halt the agent instantly.  no override.  no delay.&lt;/p&gt;

&lt;p&gt;if your agent has a kill switch that the agent can disable, you do not have a kill switch.  the halt mechanism must be external to the agent runtime and immediate.&lt;/p&gt;

&lt;p&gt;file-based kill switches work.  hardware dead man switches work.  the property is: when the human says stop, the agent stops.  before the next transaction.  not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  the test
&lt;/h2&gt;

&lt;p&gt;for any agent wallet, ask five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;can a single key compromise drain the funds?&lt;/li&gt;
&lt;li&gt;can the agent override its own spending policy?&lt;/li&gt;
&lt;li&gt;can the operator alter the action history after the fact?&lt;/li&gt;
&lt;li&gt;does the agent know what it did last session?&lt;/li&gt;
&lt;li&gt;can the agent prevent a human from shutting it down?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;if any answer is "yes", you do not have agent custody.  you have a hot wallet with a prayer.&lt;/p&gt;

&lt;h2&gt;
  
  
  reference implementation
&lt;/h2&gt;

&lt;p&gt;we build open-source agent custody infrastructure on Zcash.  FROST threshold signing, on-chain policy enforcement, Merkle-anchored attestation, semantic memory, hardware kill switches.  privacy by default.&lt;/p&gt;

&lt;p&gt;all of it is open source, MIT licensed, and verifiable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: npmjs.com/package/@frontiercompute/zcash-ika&lt;/li&gt;
&lt;li&gt;github: github.com/Frontier-Compute&lt;/li&gt;
&lt;li&gt;spec: github.com/zcash/zips/pull/1243&lt;/li&gt;
&lt;li&gt;awesome-agent-wallets: github.com/Zk-nd3r/awesome-agent-wallets&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>blockchain</category>
      <category>ai</category>
      <category>security</category>
      <category>privacy</category>
    </item>
    <item>
      <title>The Agent Custody Problem</title>
      <dc:creator>zk_nd3r</dc:creator>
      <pubDate>Wed, 08 Apr 2026 01:27:38 +0000</pubDate>
      <link>https://forem.com/zknd3r/the-agent-custody-problem-5dac</link>
      <guid>https://forem.com/zknd3r/the-agent-custody-problem-5dac</guid>
      <description>&lt;p&gt;AI agents are handling real money.  Trading bots.  Treasury managers.  Procurement agents.  Settlement systems.  They need wallets.&lt;/p&gt;

&lt;p&gt;Three products launched in the last 30 days.  Trust Wallet Agent Kit covers 25+ chains.  Coinbase AgentKit introduced the x402 protocol for agent payments.  Human.tech shipped Agentic WaaP at WalletCon.  All of them have the same problem.&lt;/p&gt;

&lt;p&gt;The agent holds a hot key.  Or the platform holds it for them.  Either way,  one compromise drains everything.&lt;/p&gt;

&lt;p&gt;This is the agent custody problem: how do you give an agent the ability to transact without giving it the ability to steal?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters now
&lt;/h2&gt;

&lt;p&gt;250,000 agents transact on-chain daily.  550+ agent projects.  $4.3B combined market cap.  The x402 payment standard,  co-founded by Coinbase and Cloudflare,  has processed $10M+ on Solana alone.  This is not a research problem.  This is a production problem.&lt;/p&gt;

&lt;p&gt;Every one of these agents holds keys.  Every one of those keys is a single point of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three requirements
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Non-drainable wallets.&lt;/strong&gt;  The signing key must never exist as a whole.  Not at creation.  Not at signing.  Not at recovery.  This is a cryptographic guarantee,  not a trust assumption.  Split-key architectures where the agent holds one share and a second party holds another.  No single compromise drains the wallet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Enforceable spend policy.&lt;/strong&gt;  Rate limits in API middleware are not policy.  A compromised agent ignores client-side checks.  Policy must live on-chain,  in a smart contract that the agent cannot bypass.  Maximum daily spend.  Approved addresses.  Position size limits.  Enforced by consensus,  not by the agent's own code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Verifiable history.&lt;/strong&gt;  Every action the agent takes must be provable after the fact.  But "provable" does not mean "public."  A trading agent that publishes every position to a public ledger gets front-run.  A treasury agent that exposes every payment gets targeted.  You need attestation that proves what happened without revealing the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why existing solutions fail
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coinbase AgentKit.&lt;/strong&gt;  Custodial.  Coinbase holds the key,  or the agent holds a hot key with an emergency admin freeze.  Better than nothing.  Still a single point of compromise at the custody layer.  No split-key signing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust Wallet TWAK.&lt;/strong&gt;  25+ chains.  220M user base.  Standard wallet model.  The agent gets full key access.  User-defined permissions exist,  but they are client-side.  A compromised agent has the key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human.tech WaaP.&lt;/strong&gt;  Two-party computation between user device and secure enclave.  Permission tokens for spending caps.  Closest to real agent custody.  But every transaction is visible on-chain.  A trading agent using WaaP broadcasts its entire strategy to the world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lit Protocol.&lt;/strong&gt;  Threshold MPC with a network of nodes.  N-of-M signing.  Weaker trust model than 2PC because you trust a threshold of nodes rather than a cryptographic split.  No attestation layer.  No privacy chain.  7,000 agent wallets created.  No way to prove what those agents did without exposing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fireblocks.&lt;/strong&gt;  Institutional MPC.  Closed source.  $699/month minimum.  Solves custody for hedge funds,  not for autonomous agents that need programmable policy and verifiable history.&lt;/p&gt;

&lt;p&gt;Every one of these products solves part of the problem.  None solves all three requirements together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What agent custody actually requires
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;2PC-MPC signing.&lt;/strong&gt;  The key is generated in two shares.  One share stays with the agent operator.  One share stays with the custody network.  At signing time,  both parties compute their part.  The full key never exists,  not in memory,  not in transit,  not in any backup.  This is not threshold signing where enough colluding nodes reconstruct the key.  This is a two-party protocol where reconstruction is cryptographically impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-chain policy.&lt;/strong&gt;  Sui Move contracts that enforce spend limits,  approved counterparties,  and position constraints.  The agent submits a transaction request.  The policy contract checks it.  If it violates the rules,  the transaction never gets signed.  No API middleware.  No client-side checks.  On-chain,  deterministic,  auditable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy attestation.&lt;/strong&gt;  Zcash shielded memos.  Every agent action gets attested to a Merkle tree anchored on Zcash mainnet.  The attestation proves the action happened.  The shielded memo hides the details.  An auditor can verify the proof.  A competitor cannot see the position.  BLAKE2b verification via EIP-152 on Ethereum at 712 gas per hash.  The same proof root,  checkable on multiple chains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-chain verification.&lt;/strong&gt;  One proof system.  Multiple verification surfaces.  The same attestation root registered on Ethereum,  Arbitrum,  Base,  Hyperliquid,  NEAR,  and Sui.  An agent custody system that only works on one chain is not an agent custody system.  Agents operate across chains.  Their custody must follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Born shielded
&lt;/h2&gt;

&lt;p&gt;We built this.  Born shielded,  traded shielded,  settled shielded.&lt;/p&gt;

&lt;p&gt;The key never existed whole.  The policy lives on Sui.  The attestation lives on Zcash.  The verification lives on seven chains.  FROST threshold signing for multi-party attestation.  EIP-152 verification on Ethereum mainnet.  54 npm exports.  2 Rust crates.  Mainnet proven.&lt;/p&gt;

&lt;p&gt;The agent custody problem is not a feature request.  It is a category.  The products that solve it will handle the money that agents move.  The products that do not will be the ones that get drained.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @frontiercompute/zcash-ika
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;zk-nd3r builds agent custody infrastructure at frontiercompute.cash  (&lt;a href="https://github.com/Frontier-Compute" rel="noopener noreferrer"&gt;https://github.com/Frontier-Compute&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>blockchain</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Verifying Zcash Proofs on Ethereum with EIP-152</title>
      <dc:creator>zk_nd3r</dc:creator>
      <pubDate>Tue, 07 Apr 2026 21:39:46 +0000</pubDate>
      <link>https://forem.com/zknd3r/verifying-zcash-proofs-on-ethereum-with-eip-152-4h80</link>
      <guid>https://forem.com/zknd3r/verifying-zcash-proofs-on-ethereum-with-eip-152-4h80</guid>
      <description>&lt;p&gt;Ethereum has a precompile that almost nobody knows about.  It lives at address &lt;code&gt;0x09&lt;/code&gt;,  it computes the BLAKE2b compression function,  and it was put there specifically so you can verify Zcash on Ethereum.  This is the story of how we use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What EIP-152 Is
&lt;/h2&gt;

&lt;p&gt;EIP-152 landed in the Istanbul hard fork (December 2019).  It exposes the BLAKE2b &lt;code&gt;F&lt;/code&gt; compression function as a precompiled contract at address &lt;code&gt;0x09&lt;/code&gt;.  Cost: 1 gas per round.  A standard BLAKE2b call runs 12 rounds,  so 12 gas total.&lt;/p&gt;

&lt;p&gt;BLAKE2b is the hash function underpinning Zcash's Sapling and NU5 Merkle trees.  Without this precompile,  computing BLAKE2b in Solidity costs around 200,000 gas.  With it: 712 gas for a full hash.  That is a 280x reduction.&lt;/p&gt;

&lt;p&gt;The EIP was proposed by Tjaden Hess and others from the Ethereum Foundation and was motivated by one thing: enabling Zcash light client verification on Ethereum without absurd gas costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;Cross chain proof verification needs hash functions.  Zcash uses BLAKE2b with personalization strings for domain separation.  Each tree level,  each protocol context uses a different personalization.  For example,  &lt;code&gt;ZcashPedersenHash&lt;/code&gt; for note commitment trees and &lt;code&gt;ZTxIdHeadersHash&lt;/code&gt; for transaction IDs.&lt;/p&gt;

&lt;p&gt;If you cannot compute these hashes cheaply on chain,  you cannot verify Zcash state on Ethereum.  Period.  The precompile makes it possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 213 Byte Input Format
&lt;/h2&gt;

&lt;p&gt;The precompile expects exactly 213 bytes,  packed tight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;rounds&lt;/strong&gt; (4 bytes): number of rounds,  12 for BLAKE2b&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;h&lt;/strong&gt; (64 bytes): state vector,  8 x uint64 little endian&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;m&lt;/strong&gt; (128 bytes): message block&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;t[0]&lt;/strong&gt; (8 bytes): offset counter low,  uint64 little endian&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;t[1]&lt;/strong&gt; (8 bytes): offset counter high,  uint64 little endian&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;f&lt;/strong&gt; (1 byte): final block flag,  0 or 1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: 213 bytes in,  64 bytes out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Precompile Call
&lt;/h2&gt;

&lt;p&gt;Here is how ZAP1Verifier.sol calls it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function blake2b(
    uint32 rounds,
    bytes memory h,
    bytes memory m,
    uint64 t0,
    uint64 t1,
    bool isFinal
) internal view returns (bytes memory) {
    bytes memory input = abi.encodePacked(
        bytes4(rounds),
        h,
        m,
        bytes8(t0),
        bytes8(t1),
        isFinal ? bytes1(0x01) : bytes1(0x00)
    );

    (bool ok, bytes memory out) = address(0x09).staticcall(input);
    require(ok, "BLAKE2b precompile failed");
    return out;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;staticcall&lt;/code&gt; to &lt;code&gt;0x09&lt;/code&gt;.  No ABI encoding.  No function selector.  Raw bytes in,  raw bytes out.  The precompile handles the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Personalization Strings
&lt;/h2&gt;

&lt;p&gt;Zcash domain separation works by XOR ing a 16 byte personalization string into the initial state vector (bytes 32 to 47 of &lt;code&gt;h&lt;/code&gt;).  Each protocol context gets its own string.  When verifying a Sapling note commitment Merkle path,  you set the personalization to the appropriate Zcash constant before each compression call.&lt;/p&gt;

&lt;p&gt;This is not optional.  Wrong personalization means wrong hash means failed verification.  The precompile does not enforce personalization;  your contract must set &lt;code&gt;h&lt;/code&gt; correctly before calling &lt;code&gt;0x09&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gas Reality
&lt;/h2&gt;

&lt;p&gt;A Merkle proof for a Zcash Sapling tree is 32 levels deep.  Each level needs one BLAKE2b compression.  With the precompile,  that is roughly 32 x 712 = ~22,800 gas for the hashing.  In pure Solidity: 32 x 200,000 = 6.4M gas.  One would blow past block gas limits.&lt;/p&gt;

&lt;p&gt;The precompile does not just save money.  It makes the verification possible at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live on Mainnet
&lt;/h2&gt;

&lt;p&gt;ZAP1Verifier is deployed on ETH mainnet at &lt;a href="https://etherscan.io/address/0x12db453A7181E369cc5C64A332e3808e807057C1" rel="noopener noreferrer"&gt;0x12db453A7181E369cc5C64A332e3808e807057C1&lt;/a&gt;.  It verifies Zcash Sapling Merkle proofs using the EIP-152 precompile.  The same contract is live on Arbitrum,  Base,  Hyperliquid,  and Sepolia.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/Frontier-Compute/zap1-verify-sol" rel="noopener noreferrer"&gt;github.com/Frontier-Compute/zap1-verify-sol&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;The precompile has been on Ethereum since 2019.  Almost nobody uses it.  We do.&lt;/p&gt;

</description>
      <category>zcash</category>
      <category>ethereum</category>
      <category>solidity</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
