<?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: Craig Solomon</title>
    <description>The latest articles on DEV Community by Craig Solomon (@craig_solomon).</description>
    <link>https://dev.to/craig_solomon</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3861408%2F39f02f6f-1ce1-419c-8a99-0b0d19a1fa28.png</url>
      <title>DEV Community: Craig Solomon</title>
      <link>https://dev.to/craig_solomon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/craig_solomon"/>
    <language>en</language>
    <item>
      <title>Using SHA-256 as an Idempotency Key in a File Anchoring Pipeline</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/using-sha-256-as-an-idempotency-key-in-a-file-anchoring-pipeline-1j1h</link>
      <guid>https://dev.to/craig_solomon/using-sha-256-as-an-idempotency-key-in-a-file-anchoring-pipeline-1j1h</guid>
      <description>&lt;p&gt;You run an ingestion job Monday. The same files get queued again Wednesday because a failed upload triggered a retry. Or two different teams independently drop the same document into your pipeline. SHA-256 doesn't care which scenario it is. The same bytes always produce the same hash. That hash is your natural idempotency key.&lt;/p&gt;

&lt;p&gt;This article builds a Python worker that uses that property to deduplicate anchoring requests before they hit the API. It also handles the case where the API already knows about a hash your local state doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Dedup Layers, Not One
&lt;/h2&gt;

&lt;p&gt;There are two places to catch duplicates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: local state.&lt;/strong&gt; A SQLite database tracking every hash you've successfully anchored. Before calling the API, check whether the hash is already in your state file. If it is, skip the call entirely. Zero API usage, zero quota consumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: the API's &lt;code&gt;duplicate_of&lt;/code&gt; field.&lt;/strong&gt; When you submit a hash that's already anchored, the API returns the new proof record plus a &lt;code&gt;duplicate_of&lt;/code&gt; field pointing to the original proof ID. This catches what local state misses: two workers running in parallel, a state file that was deleted and rebuilt, or a client who anchored the same file independently before you did.&lt;/p&gt;

&lt;p&gt;Both layers matter. Local state prevents wasted API calls. The API check is the safety net for everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The State Layer
&lt;/h2&gt;

&lt;p&gt;SQLite handles this well. JSON files don't survive concurrent writes safely.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;DB_PATH&lt;/span&gt; &lt;span class="o"&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;anchor_state.db&lt;/span&gt;&lt;span class="sh"&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;init_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        CREATE TABLE IF NOT EXISTS anchored (
            sha256      TEXT PRIMARY KEY,
            proof_id    TEXT NOT NULL,
            anchored_at TEXT
        )
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&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;already_anchored&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha256&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;dict&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="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT proof_id, anchored_at FROM anchored WHERE sha256 = ?&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;sha256&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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;row&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;proof_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchored_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&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;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sha256&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;proof_id&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;anchored_at&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT OR IGNORE INTO anchored (sha256, proof_id, anchored_at) VALUES (?, ?, ?)&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;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proof_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchored_at&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;INSERT OR IGNORE&lt;/code&gt; is the key detail. If two threads race to insert the same sha256, one wins and the other silently discards. No exception, no duplicate row, no corrupted state.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;already_anchored&lt;/code&gt; function runs before every API call. If it returns a result, the worker logs the existing proof ID and moves on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hashing and the API Call
&lt;/h2&gt;

&lt;p&gt;Large files need chunked reading. Loading gigabytes into memory at once fails in production.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hash_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&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="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&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;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&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;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&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;call_anchor_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha256&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;filename&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;api_key&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://proofledger.io/api/v1/proof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&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="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;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&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="mi"&gt;30&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="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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;Retry-After&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;60&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;print&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;Rate limited. Waiting &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;resp&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 429 retry loop reads the &lt;code&gt;Retry-After&lt;/code&gt; header and waits. Don't blindly retry on 400, 401, or 403. Those are caller errors: bad hash format, invalid key, or wrong plan tier. &lt;code&gt;raise_for_status()&lt;/code&gt; converts them to exceptions. The caller decides what to do with them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling &lt;code&gt;duplicate_of&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;filepath&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;api_key&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;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&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="n"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hash_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="c1"&gt;# Layer 1: local check
&lt;/span&gt;    &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;already_anchored&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha256&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;existing&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;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;skipped&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;proof_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proof_id&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="c1"&gt;# Layer 2: API call
&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_anchor_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;error&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;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

    &lt;span class="n"&gt;proof_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&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;id&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="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;anchored_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&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;created_at&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="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;duplicate_of&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&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;duplicate_of&lt;/span&gt;&lt;span class="sh"&gt;"&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;duplicate_of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# The canonical anchor predates this one. Record the original.
&lt;/span&gt;        &lt;span class="nf"&gt;record_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duplicate_of&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchored_at&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;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;duplicate&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;canonical_proof_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;duplicate_of&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;record_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proof_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchored_at&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;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;anchored&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;proof_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proof_id&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;When &lt;code&gt;duplicate_of&lt;/code&gt; is present, store the original proof ID, not the new one. If someone later asks "what's the proof for this file?", the answer is the earliest anchor. That's the one with the timestamp that matters for a pre-loss documentation argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring the Worker
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&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;PROOFLEDGER_API_KEY&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;sk_YOUR_KEY_HERE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCOMING&lt;/span&gt; &lt;span class="o"&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;./incoming&lt;/span&gt;&lt;span class="sh"&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&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="n"&gt;api_key&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&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="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DB_PATH&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;init_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterdir&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_file&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;Processing &lt;/span&gt;&lt;span class="si"&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;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;counts&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&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;anchored&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;skipped&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;process_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counts&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="n"&gt;status&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;result&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;proof_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;result&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;canonical_proof_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;result&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;error&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="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;  [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&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;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ref&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="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Anchored: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;anchored&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Skipped: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;skipped&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&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;Duplicate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;duplicate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Errors: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&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="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INCOMING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this against the same directory twice. The second run should show all files as &lt;code&gt;skipped&lt;/code&gt; with zero API calls. That's the behavior you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying a File After Anchoring
&lt;/h2&gt;

&lt;p&gt;Once a file is anchored, the proof lives at &lt;code&gt;https://proofledger.io/cert/&amp;lt;proof_id&amp;gt;&lt;/code&gt;. Verification doesn't need the API. The &lt;a href="https://pypi.org/project/verify-proof/" rel="noopener noreferrer"&gt;verify-proof&lt;/a&gt; package does it entirely offline:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;verify_proof&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hash_file&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;vp_hash_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verify_proof&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;load_proof&lt;/span&gt;

&lt;span class="c1"&gt;# proof.json downloaded from the certificate page or stored by your pipeline
&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_proof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence_photo_proof.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;file_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;vp_hash_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence_photo.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify_proof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proof&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;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;Verified on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;blockchain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;anchored_at&lt;/span&gt;&lt;span class="sh"&gt;'&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="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&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;Not verified: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No network connection required. Useful for forensic environments with restricted internet access, or for opposing counsel who wants to verify independently without hitting your infrastructure.&lt;/p&gt;

&lt;p&gt;Note that &lt;code&gt;verify_proof&lt;/code&gt; checks hash integrity and Merkle path structure. It does not make a live blockchain call to confirm the &lt;code&gt;tx_id&lt;/code&gt; exists on-chain. For that, use the &lt;code&gt;explorer_url&lt;/code&gt; from the proof record or &lt;code&gt;GET /api/v1/verify?hash=&amp;lt;sha256&amp;gt;&lt;/code&gt; (public, no auth, 120 requests/hr per IP).&lt;/p&gt;

&lt;h2&gt;
  
  
  A Few Production Notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Concurrent workers.&lt;/strong&gt; SQLite serializes writes but allows concurrent reads. &lt;code&gt;INSERT OR IGNORE&lt;/code&gt; handles the race correctly when two workers hash the same file at the same time. For high-volume pipelines with many parallel workers, move the state layer to Postgres. The queries above don't change, only the connection string does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State file loss.&lt;/strong&gt; If &lt;code&gt;anchor_state.db&lt;/code&gt; gets deleted, the worker falls through to Layer 2. The API catches any hashes it's already seen and returns &lt;code&gt;duplicate_of&lt;/code&gt;. You'll make more API calls than necessary on that first recovery run, but no data is lost and no incorrect anchors get created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly quota.&lt;/strong&gt; The API call still happens in the &lt;code&gt;duplicate_of&lt;/code&gt; case, and it still counts against your monthly proof quota. Local state is what prevents that consumption on repeated runs. Keep the state file around, and back it up alongside your proof records.&lt;/p&gt;




&lt;p&gt;The underlying idea is that SHA-256 is a pure function: same input, same output, every time. No coordination protocol needed. No distributed lock. The hash is the key, and the key is stable across retries, restarts, and concurrent workers.&lt;/p&gt;

&lt;p&gt;Source for the verify-proof package is at &lt;a href="https://github.com/Fulcrum-Enterprises/verify-proof" rel="noopener noreferrer"&gt;github.com/Fulcrum-Enterprises/verify-proof&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What are you using for state tracking in your file ingestion pipelines? SQLite, Postgres, Redis? Curious what other approaches people are running in production.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>C2PA Content Credentials and the Chain-of-Custody Gap</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Wed, 05 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/c2pa-content-credentials-and-the-chain-of-custody-gap-25hc</link>
      <guid>https://dev.to/craig_solomon/c2pa-content-credentials-and-the-chain-of-custody-gap-25hc</guid>
      <description>&lt;p&gt;Content Credentials are having a moment. Samsung Galaxy S25 and Google Pixel 10 sign media natively at the camera level. Adobe, Google, Meta, Microsoft, and OpenAI are all behind the standard. The EU AI Act Article 50 mandates machine-readable content marking by August 2, 2026, and C2PA is the compliance path the industry has converged on.&lt;/p&gt;

&lt;p&gt;For insurance and legal professionals, this matters. A C2PA credential embedded in an image records who captured it, when, with what device, and what processing was applied. That provenance metadata travels with the file and can be verified independently.&lt;/p&gt;

&lt;p&gt;But it has a limitation the claims and legal audience needs to understand.&lt;/p&gt;

&lt;p&gt;The credential lives inside the file.&lt;/p&gt;

&lt;p&gt;During a platform re-upload, a format conversion, or a routine file transfer, that metadata can be stripped. Not maliciously, necessarily. A workflow tool that doesn't preserve XMP metadata. A CMS that recompresses images on upload. A PDF conversion that drops sidecar data. The credential is gone, and the chain of custody has a gap, with no record it ever existed.&lt;/p&gt;

&lt;p&gt;In a claims context, that gap is significant. If an insured submits property documentation through three different software tools between capture and submission, and one strips the C2PA data, you're reviewing an unverified file. The provenance that existed at the moment of capture no longer exists at the moment of review.&lt;/p&gt;

&lt;p&gt;A blockchain anchor solves a different problem. ProofLedger computes a SHA-256 hash of the file at the moment of anchoring and records that fingerprint on Polygon (confirmed instantly) and Bitcoin (daily batch with merkle proof). The original files stay on the device. What gets recorded is a cryptographic commitment to the file's exact state at a specific point in time.&lt;/p&gt;

&lt;p&gt;That record exists on a public ledger. No platform touches it. No file transfer strips it. No party to the dispute controls it. If the file changes at all after anchoring, the hash won't match, and the mismatch is itself evidence.&lt;/p&gt;

&lt;p&gt;The strongest documentation chain uses both layers. C2PA says: here is where this file came from, how it was captured, and what was applied to it. A blockchain anchor says: this file existed, in this exact state, before this date. Together, they address different attack vectors. Neither substitutes for the other.&lt;/p&gt;

&lt;p&gt;For court authentication, FRE 901(b)(9) covers evidence produced by a process that generates an accurate result. FRE 902(13) is the self-authentication standard for machine-generated records, allowing written certification without live witness testimony. Risk managers and outside counsel who understand these rules can design documentation workflows around them before a dispute, not in response to one.&lt;/p&gt;

&lt;p&gt;C2PA compliance addresses the EU regulatory question. Blockchain anchoring addresses the chain-of-custody question. They're related, but they're not the same question.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>How Opposing Counsel Attacks Blockchain Timestamp Evidence (and What the Technical Record Shows)</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:00:03 +0000</pubDate>
      <link>https://dev.to/craig_solomon/how-opposing-counsel-attacks-blockchain-timestamp-evidence-and-what-the-technical-record-shows-49j6</link>
      <guid>https://dev.to/craig_solomon/how-opposing-counsel-attacks-blockchain-timestamp-evidence-and-what-the-technical-record-shows-49j6</guid>
      <description>&lt;p&gt;Picture this: a plaintiff's attorney submits blockchain-anchored photographs as proof that property damage existed before a policy lapsed. Defense moves to exclude them at trial. Not because the photos are doctored. Because, they argue, the timestamp can't be authenticated.&lt;/p&gt;

&lt;p&gt;That's where most blockchain-evidence disputes actually land. Not on the file's content. On whether the timestamp proves what it claims to prove.&lt;/p&gt;

&lt;p&gt;Here are the four attacks that surface in discovery and at trial, and the technical record that answers each one.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. "SHA-256 Can Be Reversed or Forged"
&lt;/h2&gt;

&lt;p&gt;This surfaces most often in a Daubert motion. The argument: cryptographic hash functions are complex and potentially fallible. If the hash can be forged, the timestamp proves nothing.&lt;/p&gt;

&lt;p&gt;The technical record: SHA-256 has no known collision vulnerabilities. NIST has certified it as a federal cryptographic standard for over two decades. A collision (two different files producing identical hashes) would require computational resources that don't exist at any practical scale. Under the Daubert framework from &lt;em&gt;Daubert v. Merrell Dow Pharmaceuticals, Inc.&lt;/em&gt;, 509 U.S. 579 (1993), and the FRE 702 reliability standard, the methodology has been tested, subjected to peer review, and its error rate is for practical purposes zero.&lt;/p&gt;

&lt;p&gt;What to prepare: an expert declaration or FRE 902(13) written certification explaining SHA-256's preimage resistance and collision resistance properties. For the Daubert motion, cite NIST publications directly. The four Daubert factors (tested, peer-reviewed, known error rate, general acceptance) are all in your favor.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. "Block Timestamps Are Unreliable"
&lt;/h2&gt;

&lt;p&gt;This one has real substance. Bitcoin consensus doesn't require precise timestamps. Nodes accept blocks whose timestamps fall within roughly two hours of the network-adjusted time. A block timestamped at 3:00 PM could have been accepted within a window on either side of that.&lt;/p&gt;

&lt;p&gt;Defense will point to this variance to argue the timestamp is too imprecise to establish whether evidence predated a specific event.&lt;/p&gt;

&lt;p&gt;Two answers. First, block confirmation depth: a transaction buried under dozens of subsequent blocks is increasingly resistant to retroactive manipulation, because reorganizing deep blocks requires growing computational resources with each additional confirmation. Second, dual-chain verification. Polygon confirms in seconds using smart-contract precision. Bitcoin confirms daily in batched merkle trees with independent node consensus. Two chains operating under different consensus mechanisms both need to be compromised to fabricate a consistent timestamp. That's the FRE 901(b)(9) argument: independent redundancy creates a process that produces an accurate result even when either chain's timing carries variance.&lt;/p&gt;

&lt;p&gt;I built the dual-chain architecture specifically because this objection is real. One chain with timing variance is attackable at a Daubert hearing. Two independent chains under different consensus models aren't. And acknowledging Bitcoin's timing window in your expert declaration is the stronger move. The Daubert inquiry rewards intellectual honesty. A declaration that explains the limitation and then explains why dual-chain confirmation answers it is more persuasive than one that pretends the limitation doesn't exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. "You Can't Prove Who Anchored It"
&lt;/h2&gt;

&lt;p&gt;Authentication of the file doesn't authenticate the anchor. Defense will argue that even if the hash is valid and the timestamp reliable, there's no proof the claiming party submitted the file at that time. Anyone could have anchored any hash to any block.&lt;/p&gt;

&lt;p&gt;This is where FRE 902(13) does the most work. The rule, effective December 1, 2017, allows a written certification from a qualified person to self-authenticate records "generated by an electronic process or system that produces an accurate result." No live expert required. The certification establishes who submitted the hash, when, which chain received it, and what transaction ID resulted.&lt;/p&gt;

&lt;p&gt;The certification should include the SHA-256 hash itself, so the opposing party can run an independent verification. Hash the original file. Compare it to the anchored hash on-chain. Confirm they match. SHA-256 is deterministic: same file, same hash, on any machine, with any tool. If they match, the files are identical. The opposing party doesn't need to trust the submitting party. They can verify against the public ledger independently.&lt;/p&gt;

&lt;p&gt;That's the FRE 901(b)(9) process-reliability argument in its cleanest form. The question isn't whether the submitting party is credible. It's whether the process (SHA-256 hash, blockchain submission, public ledger recording, merkle proof inclusion) produces an accurate result. It does, and you can prove that with the record alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. "The Record Is Hearsay"
&lt;/h2&gt;

&lt;p&gt;A blockchain transaction is an out-of-court statement offered to prove the truth of the matter asserted: that a hash existed at a given time. That's facially hearsay.&lt;/p&gt;

&lt;p&gt;Two routes through it.&lt;/p&gt;

&lt;p&gt;FRE 803(6) covers business records made at or near the time of the event by someone with knowledge, kept in the regular course of a regularly conducted activity. A records custodian or FRE 902(11) written certification lays the foundation without live testimony.&lt;/p&gt;

&lt;p&gt;FRE 902(13) handles machine-generated records directly. A record generated by a process or system that produces an accurate result is self-authenticating by written certification. No hearsay foundation required. No live witness. The certification is the foundation.&lt;/p&gt;

&lt;p&gt;In practice, 902(13) is the cleaner route because it doesn't require establishing "regular course of business." The question under 902(13) is only whether the record was generated by a reliable process. Use 803(6) as a backup for courts that want the additional business-records foundation.&lt;/p&gt;

&lt;p&gt;One thing worth noting: authentication under 901(b)(9) or 902(13) doesn't guarantee the record comes in. It still needs to survive relevance challenges and Rule 403 balancing. But getting past the hearsay objection and authentication challenge is where most blockchain-evidence disputes get resolved, and both 902(13) and 803(6) give you the tools to do it without expert testimony at trial.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means Monday Morning
&lt;/h2&gt;

&lt;p&gt;If you're building a blockchain-anchored evidence file, structure the record for each of these challenges before they arise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At anchoring:&lt;/strong&gt; Record the SHA-256 hash, both transaction IDs (Polygon and Bitcoin), block numbers, and timestamps. Keep these with the case file, not separate from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before discovery:&lt;/strong&gt; Prepare a FRE 902(13) written certification or secure a qualified expert willing to execute a declaration covering SHA-256 collision resistance, the dual-chain confirmation process, Bitcoin's timing variance and why dual-chain answers it, and the public verifiability of the anchor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For opposing counsel:&lt;/strong&gt; Give them the hash and both transaction IDs. The public verification endpoint at &lt;a href="https://proofledger.io/verify.html" rel="noopener noreferrer"&gt;proofledger.io/verify.html&lt;/a&gt; returns the full anchoring record for any hash, no account required. When opposing counsel can verify independently against the public ledger, they're no longer relying on your chain of custody. The ledger is the record. That changes the evidentiary posture.&lt;/p&gt;

&lt;p&gt;A blockchain timestamp designed to answer each of these attacks with documentation (not argument) is different from one submitted bare and defended ad hoc. Build the record before you need to use it.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Video Evidence in Disputed Claims: Why Footage Timing Is the Authentication Problem</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Fri, 31 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/video-evidence-in-disputed-claims-why-footage-timing-is-the-authentication-problem-4lbl</link>
      <guid>https://dev.to/craig_solomon/video-evidence-in-disputed-claims-why-footage-timing-is-the-authentication-problem-4lbl</guid>
      <description>&lt;p&gt;&lt;strong&gt;Meta description&lt;/strong&gt;: Dashcam footage and security video can't prove their own timing. Here's what authentication under FRE 901(b)(9) actually requires for disputed claims.&lt;/p&gt;




&lt;p&gt;A thread on r/Insurance this week illustrates the problem well. A driver gets rear-ended on the freeway. The other driver disputes liability. There's a police report. There are photos of the damage. No dashcam. The poster's conclusion: "just my word against his."&lt;/p&gt;

&lt;p&gt;The framing of "word against word" suggests the problem is credibility. It's usually not. The problem is authentication. And authentication of video and photo evidence in disputed claims is a technical and legal question that file metadata can't answer on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What File Metadata Actually Proves
&lt;/h2&gt;

&lt;p&gt;A photo or video file contains embedded timestamps. The filename might include a capture date. Dashcam footage often burns a timestamp directly into the frame. Security cameras write timestamps into the archive.&lt;/p&gt;

&lt;p&gt;None of it is independently verifiable.&lt;/p&gt;

&lt;p&gt;File creation dates can be altered in under a minute. EXIF metadata is mutable, deletable, and frequently stripped by the platforms people use to share files. A timestamp burned into dashcam footage reflects the device's internal clock, which may or may not be synchronized to an external time source, and which can be adjusted manually. Device clocks drift. They reset after battery failures.&lt;/p&gt;

&lt;p&gt;In auto claims, property claims, and premises disputes, file metadata is treated as a starting point, not a conclusion. When opposing counsel has a reason to challenge timing, these are the pressure points.&lt;/p&gt;

&lt;h2&gt;
  
  
  FRE 901(b)(9) and the Foundation Requirement for Video Evidence Authentication
&lt;/h2&gt;

&lt;p&gt;Federal Rule of Evidence 901(b)(9) allows authentication of evidence produced by a process or system by showing that the process produces an accurate result.&lt;/p&gt;

&lt;p&gt;The critical word is "showing." Foundation has to be laid. Courts have been consistent on this: 901(b)(9) authenticates a reliable process, not a timestamp field. For video evidence, the proponent has to demonstrate how the footage was captured, what time source the device used, how the file was stored and transferred, and whether it remained intact throughout.&lt;/p&gt;

&lt;p&gt;That last element, chain of custody for the digital file, is where timing disputes concentrate. A dashcam file exported to a phone, uploaded to a cloud drive, and submitted via email has moved through multiple environments before anyone asks for it in discovery. Each transfer is a potential interruption in the chain.&lt;/p&gt;

&lt;p&gt;FRE 901(b)(9) doesn't require perfection. It requires a showing that the underlying process is reliable. But the more complex the handling history, the more that showing costs, in time, in expert fees, and in litigation exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Video Evidence Timing Disputes Are Getting More Common
&lt;/h2&gt;

&lt;p&gt;Video evidence is everywhere in claims now. Doorbell cameras. Ring cameras. Security systems. Dashcams. Bodycams on delivery drivers. Footage from the utility worker who inspected the property two weeks before the loss.&lt;/p&gt;

&lt;p&gt;The footage exists. The question of when it was recorded is harder than it looks.&lt;/p&gt;

&lt;p&gt;Security camera systems are notorious for clock drift. Many facilities never synchronize their systems to an accurate external time source, and when a claim surfaces months after an incident, determining whether the timestamp is accurate requires forensic examination of the system itself. That's not always possible if the system has been replaced or updated since the incident.&lt;/p&gt;

&lt;p&gt;Dashcam footage raises a different problem. Most drivers never verify that the dashcam's internal clock is correct until they actually need the footage in a dispute. A device configured years ago may be running hours off from the actual time.&lt;/p&gt;

&lt;p&gt;For property claims, the question of whether a photo documents pre-loss condition or post-loss damage often hinges on exactly this timing gap. The photo looks the same either way. The timestamp is what distinguishes them. And the timestamp is what can be challenged.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Blockchain Anchoring Creates Independent Timing Proof
&lt;/h2&gt;

&lt;p&gt;A blockchain anchor doesn't trust the file's metadata. It creates external, independent proof of existence at a specific point in time.&lt;/p&gt;

&lt;p&gt;The mechanics: a SHA-256 hash of the file is computed locally. That hash, a unique mathematical fingerprint of the file's exact contents, gets anchored to a public blockchain. The original file never moves. Only the hash goes on-chain.&lt;/p&gt;

&lt;p&gt;The anchor is independently verifiable by anyone. Opposing counsel can verify it. An auditor can verify it. It doesn't require trusting the device, the platform, or the person who submitted the file. The anchor either matches the file or it doesn't. If it matches, the file existed in exactly its current form when the anchor was recorded.&lt;/p&gt;

&lt;p&gt;ProofLedger anchors to both Polygon, for immediate confirmation, and Bitcoin, through a daily batch process with Merkle inclusion proofs. Two independent public ledgers. Neither can be retroactively altered without controlling a majority of the computing power that has secured Bitcoin since its launch. The economic cost of that attack is not a realistic risk vector.&lt;/p&gt;

&lt;p&gt;This is what a neutral temporal authority means in practice. Not a signature on a document. An anchor on a public ledger that belongs to no one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Application: Anchoring at Capture, Not at Dispute
&lt;/h2&gt;

&lt;p&gt;The window for pre-loss anchoring is narrow and often missed.&lt;/p&gt;

&lt;p&gt;An adjuster on-site for a first inspection can hash and anchor every photo and video before leaving the property. The anchor ties each file to that day. If the claim later goes to litigation and opposing counsel challenges when the inspection photos were taken, the anchor provides a response that doesn't rely on the device clock or the file's metadata.&lt;/p&gt;

&lt;p&gt;The same principle applies before a loss happens at all. A commercial property manager running a tenant move-in inspection anchors the footage the same day. A facilities team documenting equipment condition before a contractor arrives anchors those photos at capture. The timeline becomes part of the record from the start, not a contested assertion reconstructed during discovery.&lt;/p&gt;

&lt;p&gt;Evidence Packs organize documentation by claim, case, or matter, with loss date and pre/post indicators built in. The structure reinforces what the anchor proves: this file, at this time, in this condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Authentication Under FRE 902(13)
&lt;/h2&gt;

&lt;p&gt;The anchor record itself is a machine-generated record. FRE 902(13) allows self-authentication of machine-generated records through written certification, without live expert testimony.&lt;/p&gt;

&lt;p&gt;For claims and litigation professionals, that distinction matters. Instead of scheduling expert testimony to establish how blockchain timestamping works, the party offering the anchor can authenticate it by certification. The technical foundation is documented in the certificate. Opposing counsel can object; the proponent just doesn't have to call a technical witness to get past authentication.&lt;/p&gt;

&lt;p&gt;FRE 902(13) was added in 2017. It exists precisely for machine-generated records that can be authenticated by process, not by human witness. A blockchain anchor fits that category.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Blockchain Anchor Doesn't Prove
&lt;/h2&gt;

&lt;p&gt;Authentication and credibility are different problems.&lt;/p&gt;

&lt;p&gt;A blockchain anchor proves a file existed in its current form at the time of anchoring. It doesn't prove the content is accurate, that the footage depicts what the submitting party claims, or that nobody prepared the scene before the camera captured it.&lt;/p&gt;

&lt;p&gt;A security camera video anchored before a loss proves the file existed at that date. Whether the footage actually shows pre-loss property condition as claimed, that's a separate argument, supported by the footage itself, not by the anchor.&lt;/p&gt;

&lt;p&gt;Timing is one element of a claim file. A well-anchored file with weak underlying evidence is still weak evidence. The anchor addresses the timing problem specifically. It doesn't substitute for the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Documentation Standard Before the Dispute Starts
&lt;/h2&gt;

&lt;p&gt;The dashcam discussion on r/Insurance points at something that shows up across auto, property, premises, and construction claims. The documentation exists. The timing can't be proved. The dispute survives longer than it should because nobody can cut through it with something verifiable.&lt;/p&gt;

&lt;p&gt;Anchoring at capture doesn't eliminate disputes. It eliminates one specific line of attack: the argument that nobody can verify when the documentation was created.&lt;/p&gt;

&lt;p&gt;Anchor before the loss, not after. Risk documentation, not claim documentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://proofledger.io?ref=blog" rel="noopener noreferrer"&gt;proofledger.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Idempotent File Anchoring in Python: SHA-256 as a Deduplication Key</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Thu, 30 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/idempotent-file-anchoring-in-python-sha-256-as-a-deduplication-key-1ed6</link>
      <guid>https://dev.to/craig_solomon/idempotent-file-anchoring-in-python-sha-256-as-a-deduplication-key-1ed6</guid>
      <description>&lt;p&gt;You set up an evidence file intake. Files arrive throughout the day: reports, photos, PDFs from field adjusters. Some are duplicates. The same photo emailed and then uploaded through the portal. A retry loop that fires twice because the first request timed out. A processor copying from two different source folders.&lt;/p&gt;

&lt;p&gt;If you anchor every incoming file without deduplicating first, you end up with multiple proof records for the same content. That's not a catastrophic failure. But the earlier anchor is the one that matters. It establishes when the file existed. Every anchor after the first is noise, and if your pipeline can't tell the difference, you can't reliably cite the earliest timestamp when it counts.&lt;/p&gt;

&lt;p&gt;SHA-256 solves this cleanly. Two identical files always produce the same hash. The hash is the natural idempotency key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Tiers, One Rule
&lt;/h2&gt;

&lt;p&gt;The strategy here has two stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local SQLite first.&lt;/strong&gt; Before touching any API, hash the incoming file and check a local database. If that digest is already in the table, return the existing proof record and skip the network call. Fast, offline, free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API dedup signal second.&lt;/strong&gt; If the same hash was anchored from a different client, or before this local DB existed, &lt;code&gt;POST /api/v1/proof&lt;/code&gt; returns a &lt;code&gt;duplicate_of&lt;/code&gt; field pointing to the original proof ID. That original is the canonical anchor.&lt;/p&gt;

&lt;p&gt;Both tiers enforce the same rule: the earliest anchor wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Local State
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SQLite table is straightforward. SHA-256 is the primary key.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;init_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchors.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&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="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        CREATE TABLE IF NOT EXISTS anchors (
            sha256          TEXT PRIMARY KEY,
            proof_id        TEXT NOT NULL,
            anchored_at     TEXT,
            status          TEXT,
            certificate_url TEXT
        )
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&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;conn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inserts use &lt;code&gt;INSERT OR IGNORE&lt;/code&gt;. If the hash is already present, the insert is a no-op and the first record stays.&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;store_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sha256&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;proof_id&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;anchored_at&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;status&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;certificate_url&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        INSERT OR IGNORE INTO anchors
            (sha256, proof_id, anchored_at, status, certificate_url)
        VALUES (?, ?, ?, ?, ?)
        &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;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proof_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchored_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;certificate_url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No upsert. No conflict resolution. First record wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hashing Files
&lt;/h2&gt;

&lt;p&gt;Chunk the read so large files don't blow up memory. A 4GB video and a 10KB text file take the same path.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&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="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&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;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&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;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns the hex digest you'll send to the API. The file itself goes nowhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling the API
&lt;/h2&gt;

&lt;p&gt;The v1 proof endpoint takes a SHA-256 hex digest, an optional filename, and an optional &lt;code&gt;bitcoin_requested&lt;/code&gt; flag. Auth is a Bearer token from your ProofLedger account.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="n"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://proofledger.io/api/v1/proof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_YOUR_KEY_HERE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_anchor_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&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;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&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;=&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="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/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="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest&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;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&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;filename&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;API_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="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&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="mi"&gt;30&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&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="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;resp&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&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;Anchor API failed after 3 attempts for digest &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;digest&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out.&lt;/p&gt;

&lt;p&gt;Rate-limit responses (429) parse the &lt;code&gt;Retry-After&lt;/code&gt; header before sleeping. Don't use a fixed interval. The API tells you exactly how long to wait, and that value shifts based on your tier's reset window.&lt;/p&gt;

&lt;p&gt;400, 401, and 403 are non-retryable. A malformed hash, a bad key, or a permissions mismatch won't fix itself on retry. Raise immediately and let the caller decide.&lt;/p&gt;

&lt;p&gt;Timeouts and 5xx responses fall through the loop naturally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dedup Worker
&lt;/h2&gt;

&lt;p&gt;Here's where both tiers connect.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cached_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha256&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="n"&gt;Optional&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;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT proof_id, anchored_at, status, certificate_url FROM anchors WHERE sha256 = ?&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;sha256&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&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;row&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;proof_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchored_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&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;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;certificate_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;anchor_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&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;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Connection&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="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;filename&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Tier 1: local cache
&lt;/span&gt;    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_cached_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digest&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;cached&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;source&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;local_cache&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;duplicate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Tier 2: API
&lt;/span&gt;    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;call_anchor_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# If duplicate_of is present, the API has seen this hash before.
&lt;/span&gt;    &lt;span class="c1"&gt;# Use the original proof ID, not the new record's ID.
&lt;/span&gt;    &lt;span class="n"&gt;canonical_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&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;duplicate_of&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;data&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="nf"&gt;store_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;proof_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;canonical_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;anchored_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&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;created_at&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="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&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;status&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;pending&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;certificate_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&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;certificate_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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&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;api&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;duplicate&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;duplicate_of&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proof_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;canonical_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&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;status&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;certificate_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&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;certificate_url&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;duplicate_of&lt;/code&gt; field is the API's signal that it's already seen this hash. It contains the ID of the original proof record. Store that ID, not the new record's ID.&lt;/p&gt;

&lt;p&gt;This distinction matters for evidence documentation. If a document was anchored three months ago and now arrives in a new intake batch, the relevant timestamp is the one from three months ago. That anchor establishes the file existed before whatever event is now in dispute. Citing last Tuesday's duplicate record instead is technically valid but gives the wrong timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;init_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Usage: python worker.py &amp;lt;file&amp;gt; [file ...]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;for&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;anchor_file&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="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;path&lt;/span&gt;&lt;span class="si"&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;source=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&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;duplicate=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;duplicate&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&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;proof_id=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;proof_id&lt;/span&gt;&lt;span class="sh"&gt;'&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it twice on the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python worker.py inspection-report.pdf
&lt;span class="go"&gt;inspection-report.pdf: source=api, duplicate=False, proof_id=prf_abc123

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python worker.py inspection-report.pdf
&lt;span class="go"&gt;inspection-report.pdf: source=local_cache, duplicate=True, proof_id=prf_abc123
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second run never touches the API. Same proof ID both times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending From Here
&lt;/h2&gt;

&lt;p&gt;A few natural additions depending on your pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status polling.&lt;/strong&gt; The initial anchor returns &lt;code&gt;status: "pending"&lt;/code&gt;. Poll &lt;code&gt;GET /api/v1/proof/:id&lt;/code&gt; on a background schedule and update the &lt;code&gt;status&lt;/code&gt; column when it flips to &lt;code&gt;anchored&lt;/code&gt;. The proof ID is already in your table, so this is just a lookup and an update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third-party verification.&lt;/strong&gt; Once anchored, &lt;code&gt;GET /api/v1/verify?hash=&amp;lt;sha256&amp;gt;&lt;/code&gt; is public with no API key required. Pass the hex digest and the endpoint returns the proof record and blockchain explorer links. An auditor or opposing party can check the anchor without touching your credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File-watcher integration.&lt;/strong&gt; This worker composes cleanly with a watchdog or chokidar watcher. The watcher fires on new files, calls &lt;code&gt;anchor_file()&lt;/code&gt;, and the SQLite table handles deduplication automatically. You don't need separate filename or modification-time tracking. The hash covers it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-worker state.&lt;/strong&gt; SQLite works for a single-node worker. If you're running multiple workers against the same intake folder, you'll want a shared state store. PostgreSQL with &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt; gives you the same semantics. The &lt;code&gt;canonical_id = data.get("duplicate_of") or data["id"]&lt;/code&gt; logic stays unchanged.&lt;/p&gt;

&lt;p&gt;For offline verification of anchored files, the &lt;a href="https://pypi.org/project/verify-proof/" rel="noopener noreferrer"&gt;verify-proof&lt;/a&gt; PyPI package handles local Merkle proof validation with no network call.&lt;/p&gt;




&lt;p&gt;What do you use for idempotency state in file ingestion pipelines? SQLite, Redis, Postgres? Curious how others have handled the multi-worker case without a central DB.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>How a Business Can Prove Asset Conditions Before a Hurricane, Theft, or Flood</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/craig_solomon/how-a-business-can-prove-asset-conditions-before-a-hurricane-theft-or-flood-o86</link>
      <guid>https://dev.to/craig_solomon/how-a-business-can-prove-asset-conditions-before-a-hurricane-theft-or-flood-o86</guid>
      <description>&lt;p&gt;The strongest pre-loss documentation isn't the most detailed. It's documentation that can survive a challenge about when it was created.&lt;/p&gt;

&lt;p&gt;Carriers and opposing counsel don't need to prove your photos are fabricated. They only need to raise a question about timing. If there's no independent record answering that question, the documentation's evidentiary weight can collapse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Timing Becomes the Actual Dispute
&lt;/h2&gt;

&lt;p&gt;A file's metadata timestamp is not reliable proof of when it was created. EXIF data in photos can be modified in standard editing software. File system timestamps reset when files are copied, compressed, or migrated to a new drive. Cloud storage "upload date" fields reflect when a file arrived at the platform, not when it was originally captured.&lt;/p&gt;

&lt;p&gt;In a property dispute or theft claim, this gap is commonly raised. A photo showing equipment in good condition "before" a loss becomes contested if there's no independent record establishing when that photo existed.&lt;/p&gt;

&lt;p&gt;Photographs and video are still central to any documentation package. The problem is relying on file metadata alone for the timing proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation Methods That Hold Up to Scrutiny
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Physical inspection reports with dated signatures.&lt;/strong&gt; A third-party inspector visiting the property before a loss creates a record with independent timestamps: the inspection date, the company's records, possibly a notarized signature. This is solid but expensive at scale and difficult to repeat frequently across multiple locations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notarized records and certified appraisals.&lt;/strong&gt; A notary's stamp establishes that a document existed in front of them on a specific date. For high-value individual assets, this approach works well. For routine documentation across a large property portfolio, the logistics don't scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud platform access logs.&lt;/strong&gt; Google Drive, Microsoft SharePoint, and similar services record upload timestamps and version histories. A third party controls those records, which adds some independence. The limitation: the platform can be disputed, the records aren't cryptographically immutable, and they don't constitute proof that a specific file existed in a specific form at a specific time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blockchain-anchored timestamps.&lt;/strong&gt; A SHA-256 hash of any file, whether a photograph, a video walkthrough, a PDF appraisal, or an inventory spreadsheet, can be submitted to a public blockchain. The hash is recorded on an immutable public ledger. Anyone can independently verify that the file existed in that exact form at that block's timestamp, without accessing the file itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Blockchain Anchor Actually Works
&lt;/h2&gt;

&lt;p&gt;The hash is a fixed-length fingerprint of a file's contents. It's not the file. The file stays on your device or server. Change one byte of the file and the hash changes completely. Two different files will never produce the same hash. A file that matches its on-chain hash proves it hasn't been altered since the anchor was created.&lt;/p&gt;

&lt;p&gt;ProofLedger submits that hash to two independent public blockchains: Polygon, which returns a near-instant confirmation, and Bitcoin, through a daily batch with merkle proofs. The dual-chain record matters for evidentiary purposes because the same timing claim is now supported by two independent ledgers, neither controlled by the same party. Polygon timestamping is free on every plan. Bitcoin anchoring is a per-anchor escalation, available when proof-of-work immutability is worth the cost for high-stakes documentation.&lt;/p&gt;

&lt;p&gt;What an anchor proves: the file existed in its current form at or before that block time. What the anchor doesn't prove: whether the file accurately depicts what it appears to show, or that the underlying conditions are as represented. Those are content questions. The hash is a timing and integrity proof, not a verification of what's in the image.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Legal Foundation: FRE 901(b)(9) and FRE 902(13)/(14)
&lt;/h2&gt;

&lt;p&gt;For disputes that become litigation, authentication is the central issue. FRE 901(b)(9) allows courts to authenticate evidence produced by a "process or system that produces an accurate result." A blockchain timestamp can meet this standard, but it requires laying a foundation. That typically means expert testimony or a technical certification explaining how the system works and why it reliably produces accurate output.&lt;/p&gt;

&lt;p&gt;FRE 902(13) and 902(14), added to the Federal Rules in 2017, provide a more direct path. They allow self-authentication of machine-generated records through written certification, without live expert testimony. A blockchain anchor record accompanied by a proper 902(13) or 902(14) certification can be admitted without calling a witness to establish foundation.&lt;/p&gt;

&lt;p&gt;Neither rule makes any record automatically admissible. They define the pathway. The certification still needs to be prepared correctly, and the documentation needs to meet other applicable evidentiary standards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Pre-Loss Documentation Workflow
&lt;/h2&gt;

&lt;p&gt;For businesses managing property across multiple sites, a practical pre-loss workflow combines several layers:&lt;/p&gt;

&lt;p&gt;Routine, dated photographs and video walkthroughs of key assets and conditions. Not just after a major purchase or renovation. The consistency of pre-loss photos matters as much as their detail. A single walkthrough a week before a storm is weaker than quarterly documentation going back two years.&lt;/p&gt;

&lt;p&gt;Anchor files at or near capture. If documentation exists at known times before a loss, the blockchain record establishes timing independent of file metadata or platform upload dates.&lt;/p&gt;

&lt;p&gt;Organize by location, asset, or policy period. A claim that maps to specific coverage is easier to support when documentation is grouped to match. Combining pre-loss photos, an appraisal PDF, and a current inventory spreadsheet for the same facility into a single evidence package creates a more defensible record than a folder of unrelated files with similar timestamps.&lt;/p&gt;

&lt;p&gt;Review and update documentation before each storm season or when significant asset conditions change. An anchor from several years ago may be technically valid as a timestamp. Whether it reflects conditions at the time of a specific loss is a separate question entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Case for Starting Before the Event
&lt;/h2&gt;

&lt;p&gt;Most property disputes that hinge on timing aren't fraud investigations. They're cases where documentation existed, the documentation is accurate, and there's simply no independent record placing it before the loss date. Good-faith claims fail on that narrow point.&lt;/p&gt;

&lt;p&gt;Pre-loss anchoring addresses that specific gap. It's not a substitute for physical inspections or certified appraisals. It's a layer that adds independent, verifiable timing to files a business is already creating. For businesses generating significant property documentation routinely, building that layer into the workflow before the next storm season is the move worth making now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://proofledger.io?ref=seo-blog" rel="noopener noreferrer"&gt;Start documenting before the loss with ProofLedger&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Zubulake's Preservation Trigger: The "When Did You Know" Problem Blockchain Anchoring Addresses</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/zubulakes-preservation-trigger-the-when-did-you-know-problem-blockchain-anchoring-addresses-4la0</link>
      <guid>https://dev.to/craig_solomon/zubulakes-preservation-trigger-the-when-did-you-know-problem-blockchain-anchoring-addresses-4la0</guid>
      <description>&lt;h2&gt;
  
  
  The case
&lt;/h2&gt;

&lt;p&gt;Zubulake v. UBS Warburg LLC, 217 F.R.D. 309 (S.D.N.Y. 2003), arose from an employment discrimination claim by Laura Zubulake against UBS Warburg, alleging gender discrimination and retaliation after she was passed over for a promotion and eventually terminated. The central discovery dispute involved emails stored on backup tapes, expensive to restore, with competing claims about what had been preserved, what had been destroyed, and when. The litigation produced five rulings by Judge Shira Scheindlin, collectively forming the most-cited federal framework for ESI preservation duties.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the court held
&lt;/h2&gt;

&lt;p&gt;Judge Scheindlin held that the duty to preserve evidence attaches when litigation is "reasonably anticipated." Not when a complaint is filed, and not when a formal litigation hold memo is circulated. She developed a seven-factor cost-shifting test to allocate the expense of restoring inaccessible ESI, distinguishing accessible data (producing party pays) from inaccessible data on backup tapes (subject to judicial balancing). The 2015 amendments to FRCP 37(e) displaced portions of the Zubulake sanctions framework with a uniform federal standard, but the preservation trigger analysis remains foundational.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where blockchain anchoring fits
&lt;/h2&gt;

&lt;p&gt;The "reasonably anticipated" standard creates a problem that's easier to state than to prove: how do you demonstrate, years later, that you had a specific document at a specific moment and preserved it from that point forward? In Zubulake, the dispute wasn't only whether UBS preserved the emails. It was whether what was eventually produced reflected what had actually existed and when.&lt;/p&gt;

&lt;p&gt;A SHA-256 hash anchored to two public blockchains addresses that evidentiary gap directly. The anchor doesn't change when the duty attaches. But it creates something the Zubulake framework couldn't account for: an immutable, timestamped record that a specific file existed, with specific content, at a specific moment. If a party anchors litigation-relevant documents when legal action first becomes foreseeable, they create evidence that speaks to the exact question courts ask under Zubulake: what did you have, and when?&lt;/p&gt;

&lt;p&gt;The anchor is independent of the file and independent of the producing party's own systems. It lives on two public chains regardless of what happens to the original document, regardless of backup tape failures, retention policy expiration, or system migrations that postdate the preservation trigger. Opposing counsel can verify the anchor directly against the chain without depending on the producing party's representations about what existed.&lt;/p&gt;

&lt;p&gt;Zubulake's cost-shifting analysis is also worth examining here. The court split ESI into accessible data (producing party pays) and inaccessible data on backup tapes (subject to balancing). Anchoring doesn't reduce tape restoration costs. But if a file was anchored when it was created or when it became litigation-relevant, that hash can verify any subsequently located copy without requiring full restoration of every candidate tape. Locate the file anywhere. Active systems, backup media, the opposing party's own production. Run SHA-256, compare against the anchor. Verification becomes a computation, not a credibility question.&lt;/p&gt;

&lt;p&gt;That verification can happen entirely offline. The verify-proof PyPI package lets a forensic examiner confirm a ProofLedger anchor without network calls, without server availability, and without any step that depends on a third party's uptime. The examiner hashes the file locally and checks the on-chain record. Cross-examination has very little to work with there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway for practitioners
&lt;/h2&gt;

&lt;p&gt;The "reasonably anticipated" trigger in Zubulake is a factual standard, which means it turns on what evidence exists about what a party knew and when. A blockchain timestamp creates evidence on that exact point. Not as a substitute for a litigation hold, but as a durable record showing a specific file existed with specific content at a documented moment. Under FRCP 37(e), courts evaluating whether a party took "reasonable steps to preserve" ESI look at the full factual picture. A timestamp independently verifiable on two public chains, by any party or by the court itself, is part of that picture. For authentication of the anchored records, FRE 902(14) allows self-authentication of electronic data identified by hash value via written certification. No live expert required.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Zubulake v. UBS Warburg LLC, 217 F.R.D. 309 (S.D.N.Y. 2003)&lt;/li&gt;
&lt;li&gt;FRCP 37(e) (failure to preserve electronically stored information)&lt;/li&gt;
&lt;li&gt;FRE 902(14) (self-authentication of electronic data via hash value certification)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Skip the Duplicate: Building an Idempotent File-Anchoring Worker in Python</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Thu, 23 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/skip-the-duplicate-building-an-idempotent-file-anchoring-worker-in-python-44ea</link>
      <guid>https://dev.to/craig_solomon/skip-the-duplicate-building-an-idempotent-file-anchoring-worker-in-python-44ea</guid>
      <description>&lt;p&gt;You're processing a folder of evidence files and some of them were already anchored last week. Your script crashed at file 47 and you need to rerun it. Two services are ingesting from the same drop zone at the same time.&lt;/p&gt;

&lt;p&gt;All three problems have the same root cause: no deduplication. And they all produce the same outcome: redundant submissions that waste API quota, create ambiguous records, and make downstream bookkeeping messy.&lt;/p&gt;

&lt;p&gt;The fix isn't complicated. An idempotent worker checks before it submits. And when the race window catches it anyway, it handles the &lt;code&gt;duplicate_of&lt;/code&gt; response correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the Hash Before You Touch the API
&lt;/h2&gt;

&lt;p&gt;The first move is a pre-flight check. Before calling the write endpoint, ask the public verification endpoint if the hash already exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://proofledger.io/api/v1/verify?hash=&amp;lt;sha256&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No auth required. Returns &lt;code&gt;{ "found": true, "proof": {...} }&lt;/code&gt; or &lt;code&gt;{ "found": false, "sha256": "..." }&lt;/code&gt;. Rate-limited at 120 requests per hour per IP, so it costs nothing in API quota and nothing in authentication overhead.&lt;/p&gt;

&lt;p&gt;Here's the hash computation and pre-flight:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;VERIFY_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://proofledger.io/api/v1/verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sha256_file&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="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="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&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;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&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;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&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;check_existing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_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;dict&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="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="n"&gt;VERIFY_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;file_hash&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&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;proof&lt;/span&gt;&lt;span class="sh"&gt;"&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;data&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;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;check_existing()&lt;/code&gt; returns something, you're done for that file. Store the proof ID and move on. If it returns &lt;code&gt;None&lt;/code&gt;, proceed to submit.&lt;/p&gt;

&lt;p&gt;One thing worth caching locally: once a hash is anchored, it doesn't become unanchored. So if you've seen &lt;code&gt;found: true&lt;/code&gt; for a hash, write it to disk and skip the pre-flight forever on that hash. That's how you stay well under the 120/hr rate limit across large batches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Submitting and the &lt;code&gt;duplicate_of&lt;/code&gt; Field
&lt;/h2&gt;

&lt;p&gt;Pre-flight check eliminates most duplicates. But two workers running concurrently can both see &lt;code&gt;found: false&lt;/code&gt; for the same hash and both POST it within milliseconds of each other. The API handles this case without error. When the second submission lands, the response includes a &lt;code&gt;duplicate_of&lt;/code&gt; field pointing to the ID of the earlier proof.&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;SUBMIT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://proofledger.io/api/v1/proof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_YOUR_KEY_HERE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;submit_proof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_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="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&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="n"&gt;body&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;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_hash&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;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&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;filename&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;SUBMIT_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="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&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="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;resp&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonical_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;submission&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="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;return&lt;/span&gt; &lt;span class="n"&gt;submission&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;duplicate_of&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;submission&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always store &lt;code&gt;canonical_id(submission)&lt;/code&gt;, not &lt;code&gt;submission["id"]&lt;/code&gt;. If &lt;code&gt;duplicate_of&lt;/code&gt; is present, the earlier proof is the canonical one. A third-party verifier, an auditor, or opposing counsel checking your evidence records should see the same proof ID you have on file. Using a later duplicate's ID creates a mismatch that's technically harmless but annoying to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Anchor Status
&lt;/h2&gt;

&lt;p&gt;After a successful POST, the proof sits in a pending state while the blockchain transaction propagates. You can poll &lt;code&gt;GET /api/v1/proof/:id&lt;/code&gt; (auth required, owner only) to check when it confirms:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;PROOF_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://proofledger.io/api/v1/proof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proof_id&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;max_polls&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;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval&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;15&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="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="n"&gt;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_polls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="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;PROOF_URL&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;proof_id&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="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&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;polygon_status&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchored&lt;/span&gt;&lt;span class="sh"&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;data&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_polls&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="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For batch jobs, you usually don't want to block per file waiting for confirmation. Submit everything, store the IDs, check status later. Polygon confirmation is fast. ProofLedger's Bitcoin anchoring runs on a daily batch with Merkle proofs, so that chain takes longer by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Complete Worker
&lt;/h2&gt;

&lt;p&gt;Here's the full thing: hash, pre-flight, submit with retry, handle &lt;code&gt;duplicate_of&lt;/code&gt;, save progress.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;VERIFY_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://proofledger.io/api/v1/verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;SUBMIT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://proofledger.io/api/v1/proof&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_YOUR_KEY_HERE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PROGRESS_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anchored.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sha256_file&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="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="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&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;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&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;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&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;load_progress&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;if&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;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="n"&gt;PROGRESS_FILE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROGRESS_FILE&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;f&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PROGRESS_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&lt;/span&gt;&lt;span class="p"&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;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&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;def&lt;/span&gt; &lt;span class="nf"&gt;anchor_file&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progress&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;file_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256_file&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;file_hash&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;  skip (cached): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&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="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;verify_resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="n"&gt;VERIFY_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;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;file_hash&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;verify_resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;verify_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;verify_resp&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verify_data&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;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;proof_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;verify_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proof&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="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;file_hash&lt;/span&gt;&lt;span class="p"&gt;]&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proof_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&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;pre-flight&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;  found (pre-flight): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;proof_id&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;save_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&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;headers&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;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_KEY&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="n"&gt;body&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;sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;SUBMIT_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="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&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="mi"&gt;15&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;print&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;  rate limited, sleeping &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&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="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="nf"&gt;print&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;  non-retryable &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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;path&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="k"&gt;return&lt;/span&gt;

            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&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="n"&gt;proof_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&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;duplicate_of&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;data&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="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate_of&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&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;duplicate_of&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;submitted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;file_hash&lt;/span&gt;&lt;span class="p"&gt;]&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proof_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;print&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;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;source&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;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;proof_id&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;save_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timeout&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;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&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;  timeout after 3 attempts: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&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="k"&gt;return&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_progress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;path&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;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fname&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;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&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;fname&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; files in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;directory&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="k"&gt;for&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;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;anchor_file&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="n"&gt;progress&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design notes worth calling out.&lt;/p&gt;

&lt;p&gt;Progress is keyed by hash, not by path. If a file gets renamed or moved, it won't be re-submitted. Hash identity is what the anchoring system cares about, so that's what your progress tracker should care about too.&lt;/p&gt;

&lt;p&gt;The 429 handler reads &lt;code&gt;Retry-After&lt;/code&gt; from the response header instead of sleeping a fixed amount. Hardcoded sleep intervals are almost always wrong. The server's value accounts for its current load and rate window.&lt;/p&gt;

&lt;p&gt;Non-retryable errors (400/401/403) exit immediately. A 400 means your hash is malformed. A 401/403 means your key is wrong or expired. Neither gets better with retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
python anchor_worker.py ./evidence-files/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, it hits the verify endpoint for every file it hasn't seen. On reruns, anything in &lt;code&gt;anchored.json&lt;/code&gt; is skipped entirely, no network calls. Anything found via pre-flight is recorded without touching your submission quota. Only genuinely new hashes reach the POST endpoint.&lt;/p&gt;

&lt;p&gt;If you want to check a specific file's proof later, the &lt;code&gt;id&lt;/code&gt; in your progress file maps to the certificate: &lt;code&gt;https://proofledger.io/cert/&amp;lt;id&amp;gt;&lt;/code&gt;. And the public verify endpoint works independently for anyone who needs to confirm a hash without your records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://proofledger.io/api/v1/verify?hash=&amp;lt;sha256&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That independence is the point. I made that endpoint public so your database isn't the only source of truth for a file's anchor. Anyone can check. That's what a neutral anchor is for.&lt;/p&gt;




&lt;p&gt;Interested in the Merkle proof structure that backs the Bitcoin anchoring side? Source for the verify-proof PyPI package is at &lt;a href="https://github.com/Fulcrum-Enterprises/verify-proof" rel="noopener noreferrer"&gt;https://github.com/Fulcrum-Enterprises/verify-proof&lt;/a&gt; if you want to dig into the offline verification path.&lt;/p&gt;

&lt;p&gt;What are you anchoring? Drop a comment with your use case.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Pre-Loss Evidence Documentation: The Case for Blockchain-Anchored Timestamps</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/pre-loss-evidence-documentation-the-case-for-blockchain-anchored-timestamps-2iol</link>
      <guid>https://dev.to/craig_solomon/pre-loss-evidence-documentation-the-case-for-blockchain-anchored-timestamps-2iol</guid>
      <description>&lt;p&gt;An attorney gets a discovery response with 400 photos. All timestamped the week before trial. The insured claims these were taken right after purchase, two years earlier. There's no way to verify that. The photos exist. The metadata exists. But metadata can be changed.&lt;/p&gt;

&lt;p&gt;The same gap exists across property management, construction, and risk consulting. Any professional who documents conditions before work begins or before a loss occurs is vulnerable to this problem if their timestamps can be challenged.&lt;/p&gt;

&lt;p&gt;Pre-loss evidence documentation depends on one thing: proving when a file existed, not when someone says it did. ProofLedger anchors that proof to a public blockchain before any dispute starts.&lt;/p&gt;

&lt;p&gt;Here's the workflow. A property manager, a risk consultant, a contractor doing site documentation anchors SHA-256 hashes of their files to Polygon (instant confirmation) and Bitcoin (daily batch with merkle proofs). The original files never leave the device. Only the hash is anchored. What gets created is a blockchain-anchored timestamp that proves the file existed at that exact moment in time.&lt;/p&gt;

&lt;p&gt;That timestamp changes the argument in litigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporal Verification vs. File Metadata
&lt;/h2&gt;

&lt;p&gt;Temporal verification through blockchain anchoring isn't about trusting a vendor. It's about creating a neutral temporal authority: a record that exists independently of the party who created the evidence, independently of the platform that stored it, independently of any claims system that touched it afterward.&lt;/p&gt;

&lt;p&gt;Courts can authenticate blockchain records under FRE 901(b)(9), which covers evidence produced by a process that generates an accurate result. With proper foundation, typically a technical certification explaining the anchoring method, blockchain-anchored timestamps can support authentication of when a file existed. FRE 902(13) goes further, allowing machine-generated records to be self-authenticated through written certification without live testimony.&lt;/p&gt;

&lt;p&gt;That combination matters for chain of custody in high-stakes disputes. The record doesn't need an expert to explain it at deposition if the certification is filed correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow in Practice
&lt;/h2&gt;

&lt;p&gt;A risk manager documenting a commercial property before a lease signing anchors every photo, every condition report, every inspection form. If a claim surfaces three years later, the pre-loss evidence already exists on the blockchain. The dispute shifts from "when were these taken?" to "can you challenge an immutable record secured by Bitcoin's proof-of-work since 2009?"&lt;/p&gt;

&lt;p&gt;That's a different argument. It usually goes differently.&lt;/p&gt;

&lt;p&gt;Pre-loss documentation has always mattered. What's changed is whether it can be made tamper-evident without depending on a vendor's server, a platform's API uptime, or file metadata surviving format conversion. A blockchain anchor doesn't depend on any of those. It exists on a public ledger regardless of what happens to the file afterward.&lt;/p&gt;

&lt;p&gt;Anchor before the loss, not after. Risk documentation, not claim documentation.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Notarization vs. Blockchain Timestamping: Which Fits a Claims Portfolio</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/notarization-vs-blockchain-timestamping-which-fits-a-claims-portfolio-1lb1</link>
      <guid>https://dev.to/craig_solomon/notarization-vs-blockchain-timestamping-which-fits-a-claims-portfolio-1lb1</guid>
      <description>&lt;p&gt;A commercial property manager handles risk documentation for 40 properties. Every quarter, site photos, inspection reports, and maintenance logs accumulate. Some of those files will matter if a claim surfaces two years from now.&lt;/p&gt;

&lt;p&gt;Notarization is the traditional answer to that problem. But apply it to an active portfolio with ongoing monthly documentation, and it starts to look less like a solution and more like a bottleneck.&lt;/p&gt;

&lt;p&gt;Blockchain timestamping offers a different path. Not better in every situation. Different. Understanding where each fits is what builds a workflow that holds up when a dispute goes to litigation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Notarization Actually Establishes
&lt;/h2&gt;

&lt;p&gt;A notary performs three things: verifying the identity of the signer, witnessing the execution, and recording the date and time. That seal creates a legally recognized record that a specific person executed a document at a specific time.&lt;/p&gt;

&lt;p&gt;For contracts, deeds, and sworn affidavits, that's powerful. Courts have accepted notarized documents for centuries. No foundational argument is required when opposing counsel sees the seal.&lt;/p&gt;

&lt;p&gt;But notarization has structural limits that matter at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope is narrow.&lt;/strong&gt; A notary seal attests to a signature event, not a file's integrity. If you notarize a cover page attached to 300 inspection photos, the seal covers the signing, not the contents of the photos themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing requires physical presence.&lt;/strong&gt; You can't notarize retroactively, and in many states, remote notarization carries additional authentication requirements that vary by jurisdiction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost and friction accumulate.&lt;/strong&gt; At $5-15 per notarization, a portfolio generating 50 documents per month is looking at $3,000-$9,000 per year in notarization costs alone, before travel or coordination time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Blockchain Timestamping Actually Establishes
&lt;/h2&gt;

&lt;p&gt;A blockchain timestamp doesn't touch identity. It doesn't verify who created a file or that a specific person signed anything. What it establishes is simpler and, in the right context, more useful: that a specific file, with this exact content, existed at this exact moment in time.&lt;/p&gt;

&lt;p&gt;The mechanism is a SHA-256 hash, a cryptographic fingerprint of the file's contents. That hash gets anchored to a public blockchain. The record is immutable. Anyone can verify it independently, without relying on the platform that created it.&lt;/p&gt;

&lt;p&gt;Courts can authenticate blockchain records under FRE 901(b)(9), which covers evidence produced by a process that generates an accurate result. FRE 902(13) and 902(14) go further, allowing self-authentication of machine-generated records through written certification. No live expert testimony required.&lt;/p&gt;

&lt;p&gt;For pre-loss documentation, this matters. An adjuster shows up to a loss site. The photos exist. There's no proof they were taken before the storm. A blockchain-anchored hash from three months prior changes that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scale Difference
&lt;/h2&gt;

&lt;p&gt;Here's where the workflows diverge.&lt;/p&gt;

&lt;p&gt;A notarization event takes scheduling, physical or remote coordination, and per-document fees. For a single critical document, that's manageable. For 200 photos, a maintenance log, and a drone survey updated quarterly, it's not sustainable.&lt;/p&gt;

&lt;p&gt;A blockchain timestamp takes seconds. The file never leaves the device. Only the hash is transmitted. You can batch-timestamp an entire folder of pre-loss documentation in the same time it would take to locate a notary's phone number.&lt;/p&gt;

&lt;p&gt;For portfolio-scale risk documentation, the math is simple. For individual high-stakes legal instruments, notarization's established chain of acceptance often outweighs the overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Each Belongs
&lt;/h2&gt;

&lt;p&gt;Use notarization for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signed agreements, contracts, and amendments&lt;/li&gt;
&lt;li&gt;Sworn statements and affidavits&lt;/li&gt;
&lt;li&gt;Any document where the identity of the signer is the primary question&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use blockchain timestamping for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-loss site photos and inspection records&lt;/li&gt;
&lt;li&gt;Ongoing maintenance and compliance documentation&lt;/li&gt;
&lt;li&gt;Forensic evidence that needs to establish timing without routing through a third party&lt;/li&gt;
&lt;li&gt;File volumes where per-document fees would be prohibitive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many claims operations use both. A notarized contract establishes the agreement. Blockchain-anchored inspection records establish the condition of the property at the time of inspection. They answer different questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Verification Layer
&lt;/h2&gt;

&lt;p&gt;One practical difference: blockchain timestamps are independently verifiable by anyone with the original file and a public blockchain explorer. You don't need the issuing platform to confirm anything.&lt;/p&gt;

&lt;p&gt;ProofLedger anchors to both Polygon (instant confirmation) and Bitcoin (daily batch with merkle proofs). Verification runs against &lt;code&gt;/api/v1/proof?hash=[sha256]&lt;/code&gt; and returns the anchor block, timestamp, and chain confirmation. Opposing counsel can run that check themselves without coordinating with anyone.&lt;/p&gt;

&lt;p&gt;A notarized document requires the seal, the notary's records, and potentially the notary's live testimony if authenticity is challenged. A blockchain record requires a hash and a public ledger.&lt;/p&gt;

&lt;p&gt;Neither is universally superior. The question is which authentication problem you're solving.&lt;/p&gt;




&lt;p&gt;For ongoing portfolio documentation, the operational math increasingly favors timestamping. For the documents that define the terms of a claim, notarization's centuries of legal precedent still carry weight that a newer technology is still building.&lt;/p&gt;

&lt;p&gt;The strongest documentation workflows for complex commercial claims don't choose between them. They use each for what it actually does.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Blockchain Timestamping for Pre-Loss Evidence: Why File Metadata Isn't Enough</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Fri, 17 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/blockchain-timestamping-for-pre-loss-evidence-why-file-metadata-isnt-enough-10f5</link>
      <guid>https://dev.to/craig_solomon/blockchain-timestamping-for-pre-loss-evidence-why-file-metadata-isnt-enough-10f5</guid>
      <description>&lt;p&gt;A public adjuster gets called in on a commercial property claim three weeks after a storm. The insured has photos of the roof condition taken before the event. Timestamps in the file metadata put them eight months prior. The carrier is skeptical. And without something more than metadata, it has good reason to be.&lt;/p&gt;

&lt;p&gt;The carrier's position: metadata is not proof. It's not wrong.&lt;/p&gt;

&lt;p&gt;A JPEG timestamp is a field in the file header. Anyone with basic software can change it. Courts know this.&lt;/p&gt;

&lt;p&gt;Blockchain timestamping for pre-loss documentation exists to close this gap. ProofLedger anchors a SHA-256 hash of each file to two public blockchains at the moment of capture. Polygon confirms the record instantly. Bitcoin processes a daily batch with merkle proofs. The original files never leave the device. What lands on-chain is a cryptographic fingerprint tied to a specific point in time.&lt;/p&gt;

&lt;p&gt;This is what temporal verification looks like in practice. A risk manager documents a property in January, each photo anchored as it's captured. The blockchain entry exists before any loss occurs. Months later, a storm claim comes in. The property manager has those January photos, and with them, blockchain-anchored timestamps confirming the capture date on two independent public ledgers. That's a materially different evidentiary position than metadata alone.&lt;/p&gt;

&lt;p&gt;The chain-of-custody gap in digital evidence authentication isn't hypothetical. It's a recurring issue in commercial property and casualty litigation. Defense counsel has learned to challenge file metadata as a timing source, and carriers increasingly scrutinize provenance of photos, inspection reports, and condition records. Courts can authenticate blockchain records under FRE 901(b)(9), which covers evidence produced by a process that generates an accurate result. This requires a technical foundation, typically through certification explaining how the anchoring process works. For machine-generated records, FRE 902(13) allows authentication through written certification without live witness testimony.&lt;/p&gt;

&lt;p&gt;An opposing party challenging a blockchain-anchored timestamp has to explain why two independent public ledger records are wrong. That's a harder argument than pointing to a file header that anyone could have modified.&lt;/p&gt;

&lt;p&gt;Evidence packs organize anchored files by case, claim, or matter, each with a loss date, pre/post indicators, and a verification record exportable for counsel or carrier review. The structure maps to how adjusters and attorneys actually build a file.&lt;/p&gt;

&lt;p&gt;Chain-of-custody documentation built on blockchain anchoring isn't a replacement for thorough evidence collection. It's the layer that makes the timing of that collection independently provable when metadata alone won't hold up.&lt;/p&gt;

&lt;p&gt;Anchor before the loss, not after. Risk documentation, not claim documentation.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Notarization vs. Blockchain Timestamps: What Each One Proves Under the Federal Rules</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Thu, 16 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/notarization-vs-blockchain-timestamps-what-each-one-proves-under-the-federal-rules-54nd</link>
      <guid>https://dev.to/craig_solomon/notarization-vs-blockchain-timestamps-what-each-one-proves-under-the-federal-rules-54nd</guid>
      <description>&lt;p&gt;Picture this: a contractor produces a notarized pre-construction inspection report. The property owner files a claim alleging the contractor caused structural damage during the project. The contractor's defense rests on that report, which documented pre-existing conditions before any work started.&lt;/p&gt;

&lt;p&gt;The notary stamp is authentic. The signature is genuine. No one disputes either.&lt;/p&gt;

&lt;p&gt;What opposing counsel disputes is when the inspection actually happened. The notarization date is the date someone signed the document, not the date the conditions were observed. And they're not wrong to raise it.&lt;/p&gt;

&lt;p&gt;Notarization and blockchain timestamps solve different authentication problems. Treating them as interchangeable creates gaps that surface at the worst possible moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Notarization Actually Authenticates
&lt;/h2&gt;

&lt;p&gt;Under FRE 902(8), a document acknowledged by a notary public is self-authenticating. The notary confirms two things: the identity of the signer, and that the person executed the document voluntarily in the notary's presence.&lt;/p&gt;

&lt;p&gt;That's the full scope. Notarization is identity and attestation authentication. It answers "who signed this" and "did they sign it freely." It doesn't authenticate when the underlying facts were observed, when the document was drafted, or whether the photographs attached were taken at the time described.&lt;/p&gt;

&lt;p&gt;For standard transactional documents (deeds, affidavits, contracts where timing isn't disputed), that scope is usually enough. In insurance claims, construction disputes, and pre-loss documentation, timing is often the entire dispute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap Notarization Leaves
&lt;/h2&gt;

&lt;p&gt;A notarized inspection report signed June 1 proves someone signed a document on June 1. It doesn't prove the photographs were taken on June 1. It doesn't establish that the written condition assessment reflects observations made before the first day of construction, rather than observations recorded later to fit a narrative.&lt;/p&gt;

&lt;p&gt;Underlying content can predate or postdate the signature. The notary isn't verifying the accuracy of the content, the timestamps on attached photographs, or the sequence of the inspector's work. The stamp authenticates the signature event, not the evidentiary timeline.&lt;/p&gt;

&lt;p&gt;In disputes where liability turns on when a condition existed, that gap matters more than practitioners often recognize until they're already in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Blockchain Timestamps Authenticate
&lt;/h2&gt;

&lt;p&gt;FRE 901(b)(9) allows authentication of evidence produced by a process or system that generates an accurate result. A blockchain anchor fits this standard: it's a cryptographic process that records the SHA-256 hash of a file on a distributed ledger at a verifiable moment in time.&lt;/p&gt;

&lt;p&gt;The authentication is about WHEN, not WHO.&lt;/p&gt;

&lt;p&gt;A blockchain-anchored file proves that specific data existed in its exact state at the moment of anchoring. The content can't change afterward without producing a different hash. The ledger record is permanent and public. No human attestation is required for the timestamp itself.&lt;/p&gt;

&lt;p&gt;FRE 902(13), added December 1, 2017, extends this further. It allows self-authentication of records generated by an electronic process or system through written certification. No live expert witness required. A qualified certification from the anchoring service satisfies the rule. That's the path that removes expert testimony cost from the authentication question entirely.&lt;/p&gt;

&lt;p&gt;The distinction matters: 901(b)(9) governs the process-reliability argument you'd support with expert testimony. 902(13) is the self-authentication path that bypasses the live witness requirement. Know which argument you're making before the deposition transcript is due.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dual-Chain Difference
&lt;/h2&gt;

&lt;p&gt;On the process-reliability question under 901(b)(9), the architecture of the anchoring system matters.&lt;/p&gt;

&lt;p&gt;Anchoring to Polygon and Bitcoin independently creates two transaction records on chains with different validator sets, different consensus mechanisms, and entirely different audit histories. If either chain had a systematic flaw, the other doesn't share it. That redundancy is the technical foundation for the process-reliability argument.&lt;/p&gt;

&lt;p&gt;Bitcoin uses merkle proofs: multiple files are batched into a single daily transaction, and each file receives a cryptographic proof that it was included in that batch. The merkle proof is verifiable offline, without relying on any service remaining operational. For evidentiary value that needs to hold up years from now, that architecture matters.&lt;/p&gt;

&lt;p&gt;A ProofLedger-anchored file produces both records: a Polygon transaction that settles in seconds, and a Bitcoin transaction with its merkle proof from the daily batch. Both are independently verifiable at proofledger.io/verify.html without additional software.&lt;/p&gt;

&lt;p&gt;Two independent verification paths under two independent systems is a stronger process-reliability argument than one.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You Need Both
&lt;/h2&gt;

&lt;p&gt;Some documentation scenarios call for notarization and blockchain anchoring together. Knowing when to layer them is worth having straight before the dispute.&lt;/p&gt;

&lt;p&gt;A notarized affidavit from a field inspector, combined with blockchain-anchored photographs from that inspection session, closes both authentication questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The notarization authenticates the inspector's identity and attestation under FRE 902(8).&lt;/li&gt;
&lt;li&gt;The blockchain anchors establish that those photographs existed at that specific timestamp, under FRE 901(b)(9) and 902(13).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The notary answers "who." The blockchain answers "when and exactly what." Together they cover what neither handles alone. Used independently, each leaves a gap opposing counsel will find.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means Monday Morning
&lt;/h2&gt;

&lt;p&gt;Three practical points.&lt;/p&gt;

&lt;p&gt;When advising clients on pre-loss or pre-dispute documentation, ask what the likely dispute will be about. If the question is identity and attestation, notarization may be sufficient. If the question is timing (which in property damage, construction defect, and environmental matters, it usually is), a notarized document without a timestamp anchor leaves the timing question open.&lt;/p&gt;

&lt;p&gt;Understand the FRE 902(13) self-authentication path before you need it in discovery. The rule allows written certification in place of live expert testimony. That changes the cost calculation on authentication disputes considerably. Know whether the anchoring service can provide the certification the rule requires.&lt;/p&gt;

&lt;p&gt;And don't substitute one tool for the other. They're complementary, not competing. The goal is to have both working before the dispute arises, not to litigate at trial about which one you have.&lt;/p&gt;

&lt;p&gt;Anchor before the loss, not after. Risk documentation, not claim documentation.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
  </channel>
</rss>
