<?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>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>
    <item>
      <title>Notarization vs. Blockchain Timestamp: What Each Actually Authenticates</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Wed, 15 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/notarization-vs-blockchain-timestamp-what-each-actually-authenticates-5adp</link>
      <guid>https://dev.to/craig_solomon/notarization-vs-blockchain-timestamp-what-each-actually-authenticates-5adp</guid>
      <description>&lt;p&gt;&lt;em&gt;Pre-loss evidence documentation · blockchain anchoring · notarization · FRE 901(b)(9) · FRE 902(13)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meta description:&lt;/strong&gt; Notarization proves who appeared before a notary on a given date. A blockchain timestamp proves a file existed at a specific moment. For timing disputes, those aren't the same thing.&lt;/p&gt;




&lt;p&gt;A risk manager for a commercial property firm keeps a signed, notarized inspection report for every building in the portfolio. Good practice. When a claim surfaces three years after a roof inspection, they pull the report and send it to counsel.&lt;/p&gt;

&lt;p&gt;The notarization proves the inspector appeared before a notary and attested to the document on a specific date. That's not nothing.&lt;/p&gt;

&lt;p&gt;But the dispute isn't about who signed the report. It's about whether the roof condition documented in that report predates the damage event. And on that question, the notarization is silent.&lt;/p&gt;

&lt;p&gt;This is the distinction most claims documentation guidance skips. Notarization and blockchain timestamps both sound like "proof something is real." They're not the same kind of proof.&lt;/p&gt;

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

&lt;p&gt;A notary public is a state-commissioned officer. Their function is narrow: verify the identity of the person appearing before them, witness the signing of a document, and certify the signer appeared voluntarily and knowingly.&lt;/p&gt;

&lt;p&gt;The notary stamp establishes a fact about an event that happened in front of them. Not a fact about the document's history. Not a fact about when the underlying content was created or last modified.&lt;/p&gt;

&lt;p&gt;Under FRE 902(8), acknowledged instruments are self-authenticating in federal proceedings. An acknowledged instrument accompanied by a certificate of notarization doesn't require live testimony to be admitted. That's a genuine procedural advantage.&lt;/p&gt;

&lt;p&gt;What it doesn't establish: temporal existence of the underlying file. A notarized inspection report still has the same problem as an unnotarized one. Someone drafted it at some point before the notarization. The notarization happened later. Nothing about that process proves when the inspection occurred, when the photos were taken, or whether the content was modified in the window between creation and attestation.&lt;/p&gt;

&lt;p&gt;For identity disputes, notarization works. For timing disputes, it's the wrong tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Blockchain Timestamp Establishes
&lt;/h2&gt;

&lt;p&gt;A blockchain anchor ties a SHA-256 hash to a public distributed ledger. The hash is a 64-character fingerprint derived from every byte of the file. Alter one character in a document, change one pixel in an image, and the hash changes completely.&lt;/p&gt;

&lt;p&gt;The chain writes the hash at a specific block. Independent validators confirm that block. The timestamp isn't controlled by any single party and can't be revised retroactively.&lt;/p&gt;

&lt;p&gt;What this proves: the file existed in this exact form at this moment. Not who created it. Not who held it. Just that these bytes were present somewhere in the world when the anchor was written.&lt;/p&gt;

&lt;p&gt;That's the specific claim that matters in a timing dispute. Whether the roof inspection predates the storm. Whether the site photo was taken before the loss. Whether the pre-treatment imaging existed before the procedure was contested.&lt;/p&gt;

&lt;p&gt;Dual-chain anchoring strengthens that claim. ProofLedger anchors to both Polygon for instant confirmation and Bitcoin in a daily batch with merkle proofs. Independent verification across two chains means the temporal claim doesn't rest on a single network's integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Authentication Framework in Court
&lt;/h2&gt;

&lt;p&gt;For either mechanism to be useful in a dispute, it has to satisfy authentication requirements at trial or in discovery.&lt;/p&gt;

&lt;p&gt;Notarization has a clear federal hook: FRE 902(8) provides for self-authentication of acknowledged instruments without live testimony. The notary functions as the foundation.&lt;/p&gt;

&lt;p&gt;Blockchain timestamps work under a different framework. FRE 901(b)(9) allows authentication of evidence produced by "a process or system that produces an accurate result." Blockchain meets that standard once the proponent lays the foundation: how the hash is derived, how the chain records it, why the block timestamp is reliable. Expert testimony or a written certification from the anchoring service establishes that predicate.&lt;/p&gt;

&lt;p&gt;FRE 902(13) shortens the path. It allows self-authentication of machine-generated records through written certification, without requiring a live expert at trial. A proof record from a blockchain anchoring service, paired with a written certification, can satisfy FRE 902(13). That matters when the dispute goes to deposition or a preliminary hearing and counsel needs to lay authentication without scheduling an expert.&lt;/p&gt;

&lt;p&gt;The framing distinction is worth keeping straight: FRE 901(b)(9) is about the reliability of the underlying process; FRE 902(13) is the mechanism that allows admission without live testimony. Neither is a substitute for the other in argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-Document Economics at Portfolio Scale
&lt;/h2&gt;

&lt;p&gt;The cost of notarizing a single document is low. Individual notary fees are modest. For a one-off contract or affidavit, the overhead is minor.&lt;/p&gt;

&lt;p&gt;But claims portfolios don't process individual documents. A commercial property carrier working through a busy storm season may be dealing with hundreds of inspection reports, photo bundles, field assessments, and contractor estimates across open files. Notarizing each one means scheduling notarial acts, managing originals, and tracking turnaround across a high-volume file set.&lt;/p&gt;

&lt;p&gt;More practically: notarization is an administrative step that happens downstream. The inspector doesn't call a notary at the moment the site visit ends. The photos don't get acknowledged when they're shot on location. The process happens later, when someone decides documentation needs to be formalized.&lt;/p&gt;

&lt;p&gt;Blockchain anchoring doesn't require scheduling. It runs at the moment the file is processed. An inspector finishes a site report, that file gets hashed and anchored, and the proof record exists before anyone has left the property. The cost per proof is fixed. The timing is contemporaneous with the event.&lt;/p&gt;

&lt;p&gt;For portfolios managing pre-loss documentation across many properties or claim files, that operational difference matters independently of the per-document cost comparison. Volume doesn't require proportional scheduling overhead, and timing doesn't depend on an administrative decision made later.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Both Are the Right Answer
&lt;/h2&gt;

&lt;p&gt;These mechanisms solve adjacent problems. There are scenarios where you need both.&lt;/p&gt;

&lt;p&gt;A pre-inspection report might need to establish two separate facts: who conducted the inspection and attested to the findings, and that the condition documented existed before a loss event. Notarization handles the first. A blockchain anchor made at the time of report creation handles the second.&lt;/p&gt;

&lt;p&gt;A contract executed before a construction project might need to prove identity and timing. Sign and acknowledge for identity. Anchor the signed document the same day for the temporal record.&lt;/p&gt;

&lt;p&gt;They're not competitors. Claims teams that treat them as interchangeable options are asking the wrong question. The question is what you're trying to prove and where each mechanism leaves a gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Window That Closes
&lt;/h2&gt;

&lt;p&gt;The problem with reactive documentation is that the useful window usually closes before anyone realizes it mattered.&lt;/p&gt;

&lt;p&gt;Photos taken at a site inspection aren't notarized the day they're shot. Reports aren't anchored because no one was thinking about authentication when the file was saved. The moment of capture passes, and with it the only moment when the temporal claim would be clean.&lt;/p&gt;

&lt;p&gt;A notarized affidavit produced during discovery says: "I appeared before this notary recently and attested to these documents." It doesn't say when the documents were created or whether the contents are identical to what was originally captured.&lt;/p&gt;

&lt;p&gt;An anchor made at the time of capture says: these bytes existed at this moment. The chain wrote it. No subsequent attestation changes what the record shows.&lt;/p&gt;

&lt;p&gt;That's why documentation decisions belong at intake. The anchor has to exist before the dispute, before the claim, before anyone knew the evidence would matter.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Dual-chain anchoring, evidence packs, and a public verify URL for every proof — &lt;a href="https://proofledger.io?ref=blog" rel="noopener noreferrer"&gt;proofledger.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Reliable Webhooks for Async Proof Pipelines: Signatures, Idempotency, and Replay Protection</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/reliable-webhooks-for-async-proof-pipelines-signatures-idempotency-and-replay-protection-4oa8</link>
      <guid>https://dev.to/craig_solomon/reliable-webhooks-for-async-proof-pipelines-signatures-idempotency-and-replay-protection-4oa8</guid>
      <description>&lt;p&gt;You're building a pipeline that anchors files to a blockchain. Files arrive, you hash them, you submit the hash for anchoring, and then you wait. Polygon confirmation might come back in seconds. Bitcoin confirmation runs on a daily batch. Downstream systems (case management software, audit logs, document portals) need to know when confirmation happens. You need webhooks.&lt;/p&gt;

&lt;p&gt;But a naive webhook implementation creates new failure modes: duplicate processing, replay attacks, silent failures on retry, and receivers that can't tell a legitimate event from a forged one. Four patterns address all of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  HMAC-SHA256 Signature Verification
&lt;/h2&gt;

&lt;p&gt;Your receiver needs proof that a webhook came from your system. HMAC-SHA256 ties a signature to both the payload content and a shared secret. If either changes, the signature fails.&lt;/p&gt;

&lt;p&gt;On the dispatcher side:&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;hmac&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;time&lt;/span&gt;

&lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-shared-secret&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;sign_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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;secret&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&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;separators&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;,&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;:&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;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;hashlib&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="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;build_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&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;data&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;idempotency_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;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;event&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idempotency_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the receiver side:&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;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&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;hmac&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-shared-secret&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;verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&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;secret&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;payload_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hashlib&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="nf"&gt;hexdigest&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;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhook/proof-confirmed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&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;POST&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;handle_proof_confirmed&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&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;X-Webhook-Signature&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;abort&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="c1"&gt;# continue processing...
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here. First, use &lt;code&gt;hmac.compare_digest()&lt;/code&gt; for the comparison, not &lt;code&gt;==&lt;/code&gt;. The constant-time comparison blocks timing attacks where an attacker probes the signature character by character to reconstruct it. Second, serialize with &lt;code&gt;sort_keys=True&lt;/code&gt; on the dispatcher side. JSON object key ordering varies across languages and serializers. If the dispatcher and receiver produce different byte sequences from the same dict, signatures that should match won't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency Keys
&lt;/h2&gt;

&lt;p&gt;Retries happen. Network connections drop mid-flight. Receivers return a 500 that was actually processed. Your dispatcher re-sends. Without idempotency protection, the same event processes twice and you get duplicate records in your audit log or document management system.&lt;/p&gt;

&lt;p&gt;The fix: include a stable idempotency key in every event. Derive it from the underlying fact, not from the delivery attempt. &lt;code&gt;proof_{proof_id}_anchored&lt;/code&gt; is stable across retries. A freshly generated UUID per attempt is not.&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;redis&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;IDEMPOTENCY_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;  &lt;span class="c1"&gt;# 24 hours
&lt;/span&gt;
&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhook/proof-confirmed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&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;POST&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;handle_proof_confirmed&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&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;X-Webhook-Signature&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;abort&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="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&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;idempotency_key&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;abort&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="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&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;webhook:seen:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&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;r&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;cache_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;  &lt;span class="c1"&gt;# already processed, safe to ack
&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IDEMPOTENCY_TTL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;process_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TTL keeps your Redis key space from growing indefinitely. Set it to at least as long as your retry window. If you retry for up to 4 hours, a 24-hour TTL gives you a reasonable safety margin.&lt;/p&gt;

&lt;p&gt;One edge case: what if the event processes but Redis fails before &lt;code&gt;setex&lt;/code&gt; completes? You'd process it again on the next retry. For most use cases this is acceptable (a duplicate audit entry is annoying, not catastrophic). If you need strict exactly-once semantics, wrap the Redis write and the downstream write inside a database transaction instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exponential Backoff with Jitter
&lt;/h2&gt;

&lt;p&gt;When delivery fails, you retry. But if you retry on a fixed schedule and 50 events fail at the same time (a receiver restart, a transient network issue), they all retry simultaneously. That burst can cause the receiver to fail again.&lt;/p&gt;

&lt;p&gt;Exponential backoff with jitter spaces retries out:&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;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&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;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-shared-secret&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;dispatch_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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="n"&gt;payload&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;max_attempts&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;5&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;bool&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="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&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;separators&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;,&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;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;hashlib&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="nf"&gt;hexdigest&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Webhook-Signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="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_attempts&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;url&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;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;10&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;200&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;True&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# non-retryable
&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;RequestException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&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_attempts&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;delay&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&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="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;delay&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;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The non-retryable check matters. A 401 means the signature is wrong. Retrying won't fix that. You need to investigate the shared secret or signing logic. A 400 means malformed payload. Only retry on 5xx responses and network errors where the problem might actually resolve.&lt;/p&gt;

&lt;p&gt;The jitter term (&lt;code&gt;random.uniform(0, 1)&lt;/code&gt;) breaks synchronization across concurrent dispatchers. Without it, multiple workers retrying at the same base delay still hit the receiver in a synchronized wave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replay Protection
&lt;/h2&gt;

&lt;p&gt;An attacker who intercepts a valid webhook can replay it later. The payload is signed, but the signature is still valid because neither the secret nor the content changed. For any event that triggers a state change (marking a proof as confirmed, updating a claim record), replay is a real attack surface.&lt;/p&gt;

&lt;p&gt;The timestamp window blocks this. Include a Unix timestamp in the payload and reject events where that timestamp falls outside a 5-minute window:&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;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&gt;

&lt;span class="n"&gt;REPLAY_WINDOW_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/webhook/proof-confirmed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&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;POST&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;handle_proof_confirmed&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&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;X-Webhook-Signature&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;abort&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="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;event_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&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;timestamp&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;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;event_ts&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;REPLAY_WINDOW_SECONDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;abort&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="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&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;idempotency_key&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;abort&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="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&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;webhook:seen:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&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;r&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;cache_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IDEMPOTENCY_TTL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;process_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamp check and idempotency check cover different attack surfaces. The timestamp window rejects replays from outside 5 minutes. The idempotency key catches exact duplicates within the window. You need both.&lt;/p&gt;

&lt;p&gt;Operational note: if your dispatcher and receiver clocks aren't synchronized (NTP drift is common in container environments), you'll get false rejections near the window boundary. Keep the window at 5 minutes minimum, and monitor for 400 errors that correlate with clock skew alerts on your infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Event Shape
&lt;/h2&gt;

&lt;p&gt;Here's the payload structure that ties all four patterns together:&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proof.anchored&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&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_&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="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="n"&gt;data&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;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="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;blockchain&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;polygon&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;tx_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;transaction_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;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;anchored_at_iso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The receiver verifies the signature, checks the timestamp window, checks the idempotency key, then processes. Four checks, one linear code path.&lt;/p&gt;

&lt;p&gt;These patterns apply to any async workflow where the triggering action and the confirmation happen at different times on different schedules. Blockchain anchoring is an obvious example because confirmation is inherently network-driven. I use all four in ProofLedger because Polygon and Bitcoin settle on completely different timelines, and the downstream systems that consume proof confirmations can't afford to handle duplicates or forged events.&lt;/p&gt;

&lt;p&gt;What's your approach when the event processes successfully but the idempotency write fails? Database transaction around both, a separate dedup table, or something else?&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Victor Stanley v. Creative Pipe and the Culpability Gap Blockchain Anchoring Closes</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Thu, 09 Jul 2026 13:00:02 +0000</pubDate>
      <link>https://dev.to/craig_solomon/victor-stanley-v-creative-pipe-and-the-culpability-gap-blockchain-anchoring-closes-3g27</link>
      <guid>https://dev.to/craig_solomon/victor-stanley-v-creative-pipe-and-the-culpability-gap-blockchain-anchoring-closes-3g27</guid>
      <description>&lt;h2&gt;
  
  
  The case
&lt;/h2&gt;

&lt;p&gt;Victor Stanley, Inc. v. Creative Pipe, Inc., 269 F.R.D. 497 (D. Md. 2010) is a federal discovery dispute that became one of the most-cited opinions in ESI sanctions law. Creative Pipe failed to preserve electronically stored information after the duty to preserve had clearly attached. Judge Grimm used it to build a systematic framework for courts deciding what sanctions fit what degree of preservation failure.&lt;/p&gt;

&lt;p&gt;The framework survived the parties. It's still cited when courts need to calibrate a sanctions response to a specific culpability tier.&lt;/p&gt;

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

&lt;p&gt;The opinion mapped each culpability tier from negligence through gross negligence to willful destruction, assigning remedies that scale accordingly. Negligent conduct supports lesser sanctions; willful spoliation opens the door to adverse inference instructions or case-terminating sanctions.&lt;/p&gt;

&lt;p&gt;The 2015 amendments to FRCP 37(e) later codified aspects of this graduated approach at the federal level, specifically the intent-to-deprive threshold for adverse inferences. Victor Stanley remains foundational for practitioners in circuits that apply nuanced culpability gradations beyond what 37(e) addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question courts have to answer
&lt;/h2&gt;

&lt;p&gt;Spoliation disputes come down to two things: what did the party have, and what happened to it after the duty to preserve attached?&lt;/p&gt;

&lt;p&gt;The first question is harder than it sounds. Metadata can be altered after the fact. What a custodian says they had on a specific date carries only as much weight as their credibility. File system logs are often ambiguous about the threshold question: did this document exist before the key date?&lt;/p&gt;

&lt;h2&gt;
  
  
  How a blockchain anchor changes the analysis
&lt;/h2&gt;

&lt;p&gt;A timestamp anchored to a public blockchain doesn't depend on any of that. It's a permanent, independently verifiable record that a specific file hash existed at a specific moment. No custodian testifies to it. No metadata can be retroactively adjusted.&lt;/p&gt;

&lt;p&gt;Verification is direct:&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/proof?hash=&amp;lt;sha256&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response returns the anchor transaction, block height, and timestamp. The record either exists on the ledger or it doesn't.&lt;/p&gt;

&lt;p&gt;For Victor Stanley-style disputes, the value is in what that record establishes before litigation begins. An organization that anchors key documents as part of standard retention creates an independent, court-verifiable record of what existed and when. If preservation questions arise, the blockchain record answers them without relying on custodian recollection or contested logs.&lt;/p&gt;

&lt;p&gt;Courts can authenticate these records under FRE 901(b)(9), which allows authentication of evidence produced by a process that generates an accurate result. Where self-authentication without live testimony is needed, FRE 902(13) provides a path through written certification.&lt;/p&gt;

&lt;h2&gt;
  
  
  The culpability gap
&lt;/h2&gt;

&lt;p&gt;The Victor Stanley sanctions framework is calibrated to what a court can actually prove about intent. Willful destruction draws harsher sanctions than negligence because it's harder to prove and more corrosive to the discovery process.&lt;/p&gt;

&lt;p&gt;Blockchain anchoring doesn't eliminate spoliation disputes. But it shifts the evidentiary ground. An organization with a pre-loss anchor record can demonstrate what they had, objectively, without asking a court to resolve competing custodian narratives. A party challenging that record has to contend with an immutable public ledger.&lt;/p&gt;

&lt;p&gt;The gap Victor Stanley addresses has always existed because proof of timing is genuinely difficult. Anchoring documents as they're created doesn't close that gap entirely, but it changes the nature of the dispute from credibility contest to ledger verification.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Construction Defect Claims and Burden of Proof: What Pre-Loss Documentation Has to Establish</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/construction-defect-claims-and-burden-of-proof-what-pre-loss-documentation-has-to-establish-1dlo</link>
      <guid>https://dev.to/craig_solomon/construction-defect-claims-and-burden-of-proof-what-pre-loss-documentation-has-to-establish-1dlo</guid>
      <description>&lt;p&gt;&lt;em&gt;Pre-loss documentation · blockchain anchoring · construction defect · FRE 901(b)(9) · FRE 902(13)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meta description:&lt;/strong&gt; Nevada's Supreme Court puts the burden on insureds to prove coverage in construction defect claims. Here's what pre-loss documentation has to establish and why. (158 chars)&lt;/p&gt;




&lt;p&gt;A contractor finishes a commercial renovation. Three years later, the building owner files a construction defect claim: water intrusion that allegedly began during the original build. The insurer responds with a simple question. Prove when the damage started.&lt;/p&gt;

&lt;p&gt;The owner produces photos, inspection records, maintenance logs. Every document was created after the tenant reported a problem. Nothing predates the date the insurer is disputing. The timeline stays open.&lt;/p&gt;

&lt;p&gt;This isn't a unique fact pattern. Claims Journal reported this week that the Nevada Supreme Court ruled the insured must prove coverage owed in construction defect litigation. Courts aren't going to resolve timing ambiguity in your favor. The burden sits with the party making the claim. Meeting it requires documentation that was built before the dispute, not assembled after it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why After-the-Fact Documentation Fails
&lt;/h2&gt;

&lt;p&gt;Construction defects have long latency. A waterproofing failure doesn't produce visible damage immediately. Water infiltration, settling, mold growth. These emerge over time. By the time a claim is filed, the gap between when the defect allegedly began and when anyone documented it can be years.&lt;/p&gt;

&lt;p&gt;Documentation assembled during that gap looks reactive. Photos taken three weeks before the claim was filed don't tell a court anything about what the condition was two years ago. Opposing counsel knows this. So do courts.&lt;/p&gt;

&lt;p&gt;File metadata doesn't close the gap either. EXIF timestamps in photos can be changed in under a minute with free software. The date field in a Word document reflects when the file was last saved, not when the condition it describes existed. These are routinely challenged in discovery, and for good reason.&lt;/p&gt;

&lt;p&gt;Volume doesn't help if the timing is wrong. A thousand photos with contested timestamps isn't stronger documentation than ten photos with verifiable ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Pre-Loss Documentation Has to Prove
&lt;/h2&gt;

&lt;p&gt;The shift that matters is not how much you document. It's when you establish a verifiable record.&lt;/p&gt;

&lt;p&gt;Documentation created before a dispute, before a claim, before a problem is reported: this exists independently of any litigation motive. That independence matters. Courts ask whether the process that produced the timestamp is reliable. Not whether you have a lot of records. Whether the records can be authenticated as existing when you say they did.&lt;/p&gt;

&lt;p&gt;FRE 901(b)(9) allows authentication of evidence produced by "a process or system that produces an accurate result." That's the evidentiary framework for blockchain-anchored timestamps. But 901(b)(9) requires a foundation: usually expert testimony or a qualified certification explaining the anchoring process. It doesn't self-authenticate.&lt;/p&gt;

&lt;p&gt;For that, you want FRE 902(13). Under 902(13), a written certification from a qualified person establishes authenticity without live testimony. No expert on the stand. No deposition. The certification itself does the work at trial.&lt;/p&gt;

&lt;p&gt;Know which rule you're relying on before a dispute arises. Building documentation around 902(13) compliance means generating the right certification materials at anchoring time, not trying to reconstruct them two years later in discovery.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Blockchain Anchoring Works
&lt;/h2&gt;

&lt;p&gt;ProofLedger computes a SHA-256 hash of any file and records that hash on two independent blockchains. Polygon confirms the anchor instantly. Bitcoin anchors run daily via batch, connecting individual file hashes to confirmed Bitcoin blocks through merkle proofs. Two chains, two independent verification paths, one workflow.&lt;/p&gt;

&lt;p&gt;The file never leaves the device. Only the hash goes on-chain. A SHA-256 hash is a fixed-length fingerprint of the file's contents at that moment. If a single byte changes, the hash changes. Which means an anchored hash proves the file existed, unchanged, at the time it was anchored.&lt;/p&gt;

&lt;p&gt;Each anchor produces a public verify URL. Opposing counsel can check it. An expert witness can check it. An auditor can check it. No account required. The result is either "hash found, anchored at this time on these chains" or "hash not found." There's no version of that answer that can be gamed after the fact.&lt;/p&gt;

&lt;p&gt;Bitcoin's proof-of-work immutability is the deeper layer. Altering a Bitcoin block requires redoing the computational work of every subsequent block in the chain. That's not a practical attack. It means a hash anchored to Bitcoin stays anchored, regardless of what happens to the original file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Documentation Workflow for Construction Defect Exposure
&lt;/h2&gt;

&lt;p&gt;The point of failure in most construction defect documentation isn't the absence of records. It's when the records were made.&lt;/p&gt;

&lt;p&gt;Before construction begins, document the existing property condition. Site photos, deficiencies already present, condition assessments. Anchored at the time of inspection, this establishes a baseline that predates any claimed defect. If water intrusion was already present before the contractor arrived, that's a fact the baseline can establish.&lt;/p&gt;

&lt;p&gt;During construction, anchor documentation at milestones. Structural inspections, waterproofing layers before they're covered up, punch list completion. Each anchored set creates a sequential record of what was done and when.&lt;/p&gt;

&lt;p&gt;After any complaint or remediation, anchor immediately. The timestamp on the anchor is the evidentiary fact. Documenting a repair thoroughly and anchoring two weeks later loses the benefit. The gap is the problem.&lt;/p&gt;

&lt;p&gt;Evidence packs organize anchored files by case, claim, or matter, with loss dates and pre/post indicators. When a claim arrives, the timeline is already structured rather than scattered across drives and email threads with contested metadata.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the Insured's Burden Changes the Calculation
&lt;/h2&gt;

&lt;p&gt;Construction defect litigation is slow. Discovery windows are long. Documentation that looked adequate at claim filing can look thin three years in, when opposing counsel has worked through every metadata field and every file creation date.&lt;/p&gt;

&lt;p&gt;The Nevada ruling is a useful frame for how courts approach this. The insured must prove coverage owed. That means proving not just what the condition was, but when it existed relative to the policy period. An adjuster's inspection report that can't be placed in time is documentation without authentication. It shows something. It doesn't prove when.&lt;/p&gt;

&lt;p&gt;An anchored inspection report, site photos anchored the day they were taken, a maintenance log anchored the day it was written: these aren't just evidence. They're verifiable facts on two independent public ledgers. The same answer comes back for both parties. That's what neutral temporal authority means in practice. The timestamp isn't a claim someone is making. It's a mathematically settled fact.&lt;/p&gt;

&lt;p&gt;Documentation assembled after the fact is documentation assembled under litigation motive. Courts understand this distinction. Anchored records that predate any dispute don't carry that problem.&lt;/p&gt;

&lt;p&gt;If your workflow involves construction defect exposure, the time to build the record is before anything goes wrong. 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;Start documenting your construction projects before disputes arise at proofledger.io&lt;/a&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>The Original File Problem: What FRE 1002 Actually Requires When Digital Evidence Is Disputed</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Mon, 06 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/the-original-file-problem-what-fre-1002-actually-requires-when-digital-evidence-is-disputed-p5c</link>
      <guid>https://dev.to/craig_solomon/the-original-file-problem-what-fre-1002-actually-requires-when-digital-evidence-is-disputed-p5c</guid>
      <description>&lt;p&gt;Picture this: an adjuster photographs storm damage on day one. Three months later, those photos live on a carrier's cloud backup. At deposition, opposing counsel asks whether the photos produced in discovery are the originals or backup copies. The adjuster can't say with certainty. The phone was replaced. The cloud platform may have applied compression. Opposing counsel moves to exclude on best-evidence grounds.&lt;/p&gt;

&lt;p&gt;This is the FRE 1002 problem for digital evidence. It comes up more than practitioners expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Best Evidence Rule Requires
&lt;/h2&gt;

&lt;p&gt;FRE 1002 states that to prove the content of a writing, recording, or photograph, the original is required.&lt;/p&gt;

&lt;p&gt;FRE 1001(d) defines "original" for electronic evidence: the recording itself, or any counterpart intended to have the same effect. A digital photograph is its original byte sequence. A compressed copy, a re-encoded version, or a file that passed through a platform's processing pipeline may or may not be the same item.&lt;/p&gt;

&lt;p&gt;FRE 1003 provides practical relief: a duplicate is admissible to the same extent as an original unless a genuine question is raised about the original's authenticity, or admission would be unfair. Courts don't exclude duplicates automatically. But once opposing counsel raises a credible authenticity question, the burden shifts to the proponent to answer it.&lt;/p&gt;

&lt;p&gt;FRE 1004 allows other evidence when the original has been lost without bad faith, is unobtainable through judicial process, or is in an opponent's possession. None of these provisions resolve the core problem: when the proponent can't establish that the file they're offering is byte-for-byte identical to what they originally captured, the authentication question stays open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Digital Evidence Creates the Gap
&lt;/h2&gt;

&lt;p&gt;The gap isn't usually fraud. It's workflow.&lt;/p&gt;

&lt;p&gt;A claims professional photographs a flooded warehouse. The photos go from camera roll to email to a shared drive to a claims management system. Each step is an opportunity for platform compression, automatic re-encoding, or metadata stripping. By the time litigation starts two years later, the "original" may not exist on any accessible device. What's available is a cascade of copies.&lt;/p&gt;

&lt;p&gt;Under FRE 1003, any of those copies can be attacked. Opposing counsel doesn't need to prove the copies were tampered with. They need to raise a genuine question. That question is easy to raise when the chain between device and courtroom has several unmonitored steps.&lt;/p&gt;

&lt;p&gt;And most claims workflows have no point where anyone asks: is this still the file we started with?&lt;/p&gt;

&lt;p&gt;The file doesn't remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Hash Establishes
&lt;/h2&gt;

&lt;p&gt;A SHA-256 hash is deterministic. The same file always produces the same hash. A file that has changed by even one byte produces a completely different hash. This is not metadata. It's not a field someone typed into the file's header. A hash is a mathematical output derived from the entire byte sequence of the file.&lt;/p&gt;

&lt;p&gt;When a file is hashed and that hash is anchored to a public blockchain, two things become independently verifiable later: the exact byte content of the file at the time of anchoring, and when that hash was committed to the chain.&lt;/p&gt;

&lt;p&gt;When the file is produced in discovery, hash it again. If the output matches the on-chain record, the authenticity question under FRE 1003 has an answer. The file hasn't changed. Not one byte. That's math, not testimony.&lt;/p&gt;

&lt;p&gt;I built ProofLedger to anchor hashes to both Polygon and Bitcoin because single-chain verification leaves the process-reliability argument on a single foundation. Two independent public ledgers, both recording the same hash, make the FRE 901(b)(9) case considerably stronger. The public verify endpoint at &lt;a href="https://proofledger.io/verify.html" rel="noopener noreferrer"&gt;proofledger.io/verify.html&lt;/a&gt; is accessible to any third party without authentication, which matters when opposing counsel or a court-appointed examiner wants to check the hash independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laying the Authentication Foundation
&lt;/h2&gt;

&lt;p&gt;Introducing a hash comparison at trial requires its own evidentiary foundation. FRE 901(b)(9) is the mechanism: authentication via evidence produced by a process or system that generates an accurate result. The blockchain anchoring process, with deterministic cryptographic operations and a public ledger record, fits that framework.&lt;/p&gt;

&lt;p&gt;FRE 902(13) allows self-authentication of machine-generated records via written certification, eliminating the need for live expert testimony. That matters in budget-constrained litigation where an expert deposition is a real cost.&lt;/p&gt;

&lt;p&gt;This isn't automatic. The proponent still needs to establish what SHA-256 does, what the blockchain record represents, and how the verification process works. But that foundation is available and defensible. EXIF metadata doesn't have an equivalent foundation. It can be changed after the fact. The on-chain hash record cannot.&lt;/p&gt;

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

&lt;p&gt;Three practical steps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hash and anchor at capture.&lt;/strong&gt; The gap between file creation and anchoring is the gap opposing counsel will attack. Don't wait until the claim is filed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the original file.&lt;/strong&gt; The hash proves the file hasn't changed, but only if the original still exists to re-hash. Platform compression shouldn't be the only surviving copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brief your documentation team on the FRE 1003 attack surface.&lt;/strong&gt; The question "is this the original file?" should have a reliable answer at every stage of a claim. In most workflows right now, it doesn't.&lt;/p&gt;

&lt;p&gt;The best evidence rule was designed for paper. Digital files have properties paper doesn't: they copy silently, platforms alter them without notice, and the chain between capture and courtroom is long. An on-chain hash is the one thing that can answer the original-versus-copy question with something other than "I think so."&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>How to Create Tamper-Proof Records for Pre-Loss Documentation</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Fri, 03 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/how-to-create-tamper-proof-records-for-pre-loss-documentation-8p3</link>
      <guid>https://dev.to/craig_solomon/how-to-create-tamper-proof-records-for-pre-loss-documentation-8p3</guid>
      <description>&lt;p&gt;An attorney receives a document production request. Among the files: 200 photos the property owner claims were taken before the loss. The file timestamps say what they say. But file timestamps can be changed in seconds by anyone with basic computer access.&lt;/p&gt;

&lt;p&gt;That's the problem with digital evidence right now. The evidence may be genuine. The documentation around it almost certainly isn't verified. When the dispute hits discovery, chain of custody becomes the entire argument.&lt;/p&gt;

&lt;p&gt;ProofLedger was built for this gap: pre-loss documentation that creates a verifiable record before anyone disputes the timeline.&lt;/p&gt;

&lt;p&gt;When you anchor a file in ProofLedger, the original document never leaves your device. The platform generates a SHA-256 hash of the file, a mathematical fingerprint unique to that exact file at that exact moment. That hash gets anchored to two public blockchains: Polygon for instant confirmation, and Bitcoin's proof-of-work network for daily batch anchoring with merkle proofs.&lt;/p&gt;

&lt;p&gt;The result is a tamper-proof record that exists independently of the file itself. You can't change it, no platform can strip it, and no format conversion can corrupt it.&lt;/p&gt;

&lt;p&gt;Chain of custody for digital evidence has always had a weak link: proving when a file was captured. File metadata is mutable. Cloud storage timestamps depend on server accuracy, screenshots can be backdated, and neither method produces a record on a ledger that no party to the dispute controls. A blockchain timestamp works differently. The anchor transaction appears on a public ledger at a specific block height, in sequence with every other transaction before and after it. That sequence cannot be altered without rewriting the entire chain.&lt;/p&gt;

&lt;p&gt;For evidence authentication purposes, blockchain records can be authenticated under FRE 901(b)(9) as output from a process that produces an accurate result. FRE 902(13) allows machine-generated records to be self-authenticated through written certification, without requiring live expert testimony in every proceeding.&lt;/p&gt;

&lt;p&gt;Evidence preservation before the loss is where this matters most. A risk manager who photographs a facility hazard in February needs to prove those photos existed before the March incident. For an insured documenting property condition before a storm, the same question: can you prove the record predates the claim? A blockchain anchor answers that question on a public ledger no party controls.&lt;/p&gt;

&lt;p&gt;Evidence packs in ProofLedger let you organize blockchain evidence by case, matter, or claim, with loss dates and pre/post indicators built in. When files get produced in discovery, the anchor transaction travels with them.&lt;/p&gt;

&lt;p&gt;A property manager documents roof condition before tenant occupancy. Photos, inspection reports, contractor notes: all anchored. Six months later, the tenant claims pre-existing damage. The property manager's files show what the roof looked like before occupancy. The blockchain anchor proves the record predated move-in. Chain of custody intact. Timeline verified.&lt;/p&gt;

&lt;p&gt;That's what tamper-proof records provide. Not a claim the evidence is authentic. A cryptographic proof on a public ledger that anyone can verify independently.&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>Pippins v. KPMG: Proportional Preservation and the Gap Anchoring Fills</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Thu, 02 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/pippins-v-kpmg-proportional-preservation-and-the-gap-anchoring-fills-3dcc</link>
      <guid>https://dev.to/craig_solomon/pippins-v-kpmg-proportional-preservation-and-the-gap-anchoring-fills-3dcc</guid>
      <description>&lt;h2&gt;
  
  
  The case
&lt;/h2&gt;

&lt;p&gt;In Pippins v. KPMG LLP, 279 F.R.D. 245 (S.D.N.Y. 2012), former audit associates brought a class action alleging KPMG violated the Fair Labor Standards Act. As discovery opened, plaintiffs sought a preservation order requiring KPMG to retain the individual hard drives of every former associate whose work might fall within the relevant period. The scope question reached Magistrate Judge Cott.&lt;/p&gt;

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

&lt;p&gt;Magistrate Judge Cott declined to require that level of preservation. The court held that preservation obligations must be proportional to the case's needs, the relevance of what's being preserved, and the burden on the party required to preserve it. Pippins remains a frequently cited limiting principle on sweeping hardware retention demands, standing for the proposition that courts won't impose preservation costs disproportionate to the likely evidentiary value of the material.&lt;/p&gt;

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

&lt;p&gt;The Pippins holding is practically sensible. Requiring a company to warehouse hundreds of individual hard drives from former employees, potentially for years, would impose costs that courts are right to check. But the limiting principle carries a cost of its own. When physical media isn't preserved under a proportionality ruling, what's lost isn't just the hardware. The authentication record for what was on that hardware becomes dependent on secondary evidence. And secondary evidence, in litigation where file content or timing is contested, is a harder argument than a verifiable artifact that predates the dispute.&lt;/p&gt;

&lt;p&gt;SHA-256 hash anchoring doesn't preserve the drive. It creates a permanent, on-chain record that a specific file, with specific byte-level content, existed at a documented moment. That record goes to Polygon for near-immediate confirmation and to Bitcoin in a daily batch with merkle proofs. Both chains are public and independently verifiable at &lt;a href="https://proofledger.io?ref=case_study" rel="noopener noreferrer"&gt;proofledger.io&lt;/a&gt;. The proof lives on the ledger regardless of what happens to the original hardware.&lt;/p&gt;

&lt;p&gt;What the anchor establishes: file integrity (the content is byte-for-byte unchanged from the moment of anchoring) and a timestamp (the hash was recorded before or after a given date). What it doesn't establish: that the file still exists in its current form, or that the content is accurate. The authentication argument for a blockchain record runs under FRE 901(b)(9), which allows evidence produced by a process that generates an accurate result to be authenticated by foundation describing how that process works. The SHA-256 algorithm and the dual-chain anchoring mechanism are the process. The on-chain transaction record is the output.&lt;/p&gt;

&lt;p&gt;For third-party verification without any platform dependency, the verify-proof Python package supports fully offline checks. It takes a local file and a proof record, computes the SHA-256 hash locally, and confirms or rejects the match against the on-chain record. Opposing counsel and auditors can run that verification independently. Nothing requires trusting a portal.&lt;/p&gt;

&lt;p&gt;Take the Pippins scenario to its logical end. If KPMG's former associates had anchored their work files at creation, the court's proportionality ruling on hardware retention would be unchanged. What would be different: the authentication record wouldn't depend on the hardware decision. The hash anchor exists on a public blockchain. Whether the drive was preserved becomes a separate question from whether the file's content is independently verifiable.&lt;/p&gt;

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

&lt;p&gt;Proportional preservation doctrine protects parties from unreasonable retention burdens. But it doesn't generate the authentication record you need when the original media is gone and the content is disputed. FRE 902(14) allows authentication of electronic data identified by hash value through written certification. FRE 901(b)(9) supports the process reliability argument for blockchain records. Anchoring file hashes before any preservation duty attaches keeps practitioners out of the FRCP 37(e) sanctions analysis entirely, because the authentication record doesn't live on hardware subject to proportionality limits. It lives on a public ledger, independent of any retention decision.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Pippins v. KPMG LLP, 279 F.R.D. 245 (S.D.N.Y. 2012)&lt;/li&gt;
&lt;li&gt;FRE 901(b)(9): authentication based on process or system producing accurate result&lt;/li&gt;
&lt;li&gt;FRE 902(14): self-authentication of electronic data identified by hash value&lt;/li&gt;
&lt;li&gt;FRCP 37(e): sanctions for failure to preserve electronically stored information&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Chain of Custody Gaps in Digital Evidence: How Blockchain Timestamps Close Them</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Tue, 30 Jun 2026 00:05:00 +0000</pubDate>
      <link>https://dev.to/craig_solomon/chain-of-custody-gaps-in-digital-evidence-how-blockchain-timestamps-close-them-36me</link>
      <guid>https://dev.to/craig_solomon/chain-of-custody-gaps-in-digital-evidence-how-blockchain-timestamps-close-them-36me</guid>
      <description>&lt;p&gt;A risk manager documents a parking lot hazard in March. Cracks in the asphalt, photographed and filed in a SharePoint folder. Six months later, a slip-and-fall claim arrives. The plaintiff's attorney argues the damage was present for over a year and the property owner failed to act.&lt;/p&gt;

&lt;p&gt;The risk manager has photos. File metadata can be altered. A SharePoint timestamp isn't evidence. ProofLedger exists for exactly this gap: blockchain timestamps that prove when digital evidence existed, on a public ledger that nobody controls.&lt;/p&gt;

&lt;p&gt;This is the core problem with evidence preservation in insurance and legal work. The documentation exists. The dates exist. But without a tamper-proof record tied to an independent source, the evidence can be challenged at any time.&lt;/p&gt;

&lt;p&gt;Here's how the workflow runs. Drag site inspection files into the platform and it generates a SHA-256 hash of each one, then anchors that hash to two public blockchains: Polygon for instant confirmation within seconds, and Bitcoin for proof-of-work immutability on the oldest continuously running decentralized network. Your original files never leave your device. Only the hash goes on-chain. What comes back is a blockchain evidence record with a timestamp nobody can alter after the fact.&lt;/p&gt;

&lt;p&gt;For evidence authentication in litigation, courts can admit blockchain-anchored records under FRE 901(b)(9), which covers evidence produced by a process or system that generates an accurate result. Under FRE 902(13), machine-generated records can be self-authenticated through written certification. No live expert testimony required. That distinction matters when a claims team is trying to resolve a dispute without scheduling a forensics expert.&lt;/p&gt;

&lt;p&gt;Files can be organized into evidence packs by case, claim, or matter, with each item marked as pre-loss or post-loss. That structure makes the chain of custody readable during discovery, without reconstructing a timeline from individual file dates.&lt;/p&gt;

&lt;p&gt;Pre-loss documentation is the use case this workflow was built for. A risk manager runs site inspection photos through the anchoring process before filing anything. A public adjuster anchors their scene documentation on arrival, before remediation starts. A contractor photographs completed work and anchors it at project close. Each anchor is a blockchain evidence record that predates any future dispute.&lt;/p&gt;

&lt;p&gt;The distinction between pre-loss and post-loss records is often the dispute itself. Chain of custody gaps don't show up until someone challenges the timeline. By then, going back to establish when the evidence actually existed isn't an option.&lt;/p&gt;

&lt;p&gt;Standard file metadata doesn't hold under adversarial conditions. It's editable by design. Forensic witnesses can testify to that, which means every unanchored file is a potential vulnerability in a coverage dispute or a litigation timeline.&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>Batch File Anchoring in Python: Concurrency, Rate Limits, and Resumable Progress</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Tue, 30 Jun 2026 00:00:04 +0000</pubDate>
      <link>https://dev.to/craig_solomon/batch-file-anchoring-in-python-concurrency-rate-limits-and-resumable-progress-5a6i</link>
      <guid>https://dev.to/craig_solomon/batch-file-anchoring-in-python-concurrency-rate-limits-and-resumable-progress-5a6i</guid>
      <description>&lt;p&gt;You have a directory of evidence files that need blockchain-anchored timestamps. Maybe it's a pre-loss documentation archive. Maybe it's a set of site inspection photos captured before a dispute filed.&lt;/p&gt;

&lt;p&gt;A for-loop over the directory will work until it doesn't. Hit a rate limit, crash without saving state, and you're starting over with no record of what actually made it through.&lt;/p&gt;

&lt;p&gt;This is the worker pattern I'd reach for when bulk archiving through ProofLedger's v1 API. Three things the naive loop skips: concurrent submissions, 429 backoff that reads the &lt;code&gt;Retry-After&lt;/code&gt; header, and resumable progress so a crash doesn't reset the whole job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hash First, Then Submit
&lt;/h2&gt;

&lt;p&gt;The ProofLedger v1 API anchors a SHA-256 digest, not the file itself. The file stays on your machine. Step one is always local hashing.&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;hash_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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chunked reads keep memory flat regardless of file size. A 4GB video and a 10-page PDF go through the same function.&lt;/p&gt;

&lt;p&gt;The base API call takes the digest in the request body, with your API key in the &lt;code&gt;Authorization&lt;/code&gt; header.&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;requests&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;anchor_hash&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="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="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;payload&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="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;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;payload&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="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;That's fine for a single file. A batch job needs real error handling on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling 429s
&lt;/h2&gt;

&lt;p&gt;The API is rate-limited. When you're submitting files concurrently, you'll hit 429s. The right response is to back off and retry, reading the &lt;code&gt;Retry-After&lt;/code&gt; header rather than guessing.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;anchor_hash&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="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="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;payload&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="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="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;6&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;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;payload&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;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;RequestException&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;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&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;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="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&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;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;wait&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. Waiting &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s (attempt &lt;/span&gt;&lt;span class="si"&gt;{&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;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/6)&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;wait&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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;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;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;Anchoring failed for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; after 6 attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 400/401/403 group is non-retryable. A 400 means your hash is malformed. A 401 or 403 means your key is wrong or on the wrong plan tier. Retrying those wastes time and burns rate limit. The &lt;code&gt;requests.RequestException&lt;/code&gt; branch handles network timeouts and connection drops separately from HTTP status codes.&lt;/p&gt;

&lt;p&gt;Six attempts is enough ceiling to clear a sustained rate-limit window without hanging the job indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ThreadPoolExecutor&lt;/code&gt; with five workers gets you parallel submissions without hammering the endpoint hard enough to trigger a flood of 429s.&lt;/p&gt;

&lt;p&gt;The choice of &lt;code&gt;as_completed&lt;/code&gt; over &lt;code&gt;executor.map&lt;/code&gt; matters here. With &lt;code&gt;executor.map&lt;/code&gt;, you'd have to wait for all futures before processing any results. With &lt;code&gt;as_completed&lt;/code&gt;, each resolved future triggers a disk write immediately. If the script crashes, you restart and pick up where the last successful write landed.&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;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;as_completed&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;MAX_WORKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;path&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&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;hash_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;path&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_hash&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&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;path&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;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five concurrent threads also keeps log output readable. Bump &lt;code&gt;MAX_WORKERS&lt;/code&gt; to 20 and you get 20 interleaved error messages whenever something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resumable Progress
&lt;/h2&gt;

&lt;p&gt;Write to disk after every successful anchor. Every one. The progress file is cheap to write; restarting an entire archive job is not.&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;json&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;anchor_progress.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;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="n"&gt;p&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;PROGRESS_FILE&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&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="nc"&gt;Path&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="nf"&gt;write_text&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;dumps&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The progress dict is keyed by file path. Each entry stores the SHA-256 digest, proof ID, status, and certificate URL. Any path already in the file is skipped on startup.&lt;/p&gt;

&lt;p&gt;One caveat: this assumes files in your archive are immutable after capture. If a file might get overwritten between runs, you'd want to re-hash and compare the stored digest before deciding to skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The duplicate_of Response
&lt;/h2&gt;

&lt;p&gt;If you submit a hash that's already been anchored, the API returns a &lt;code&gt;duplicate_of&lt;/code&gt; field pointing to the original proof ID.&lt;/p&gt;

&lt;p&gt;It can come from multiple places. Two team members photographing the same document separately. A file copied into multiple subdirectories before ingestion. The same report exported under two filenames.&lt;/p&gt;

&lt;p&gt;When you see &lt;code&gt;duplicate_of&lt;/code&gt;, the original proof is canonical. Don't create a new anchor for identical bytes.&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;record_result&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="n"&gt;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;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;result&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;entry&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="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;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="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;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;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;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;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;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="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="n"&gt;entry&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="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;duplicate_of&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;  Duplicate: &lt;/span&gt;&lt;span class="si"&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;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; original &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_of&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;  Anchored: &lt;/span&gt;&lt;span class="si"&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;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;name&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;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="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;progress&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;certificate_url&lt;/code&gt; from the original anchor is what you'd attach for chain-of-custody documentation. The &lt;code&gt;duplicate_of&lt;/code&gt; field gives you the ID to find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Script
&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;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;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;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;as_completed&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_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;anchor_progress.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MAX_WORKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;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="n"&gt;p&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;PROGRESS_FILE&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&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="nc"&gt;Path&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="nf"&gt;write_text&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;dumps&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;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_hash&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="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="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="n"&gt;payload&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="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="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;6&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;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;payload&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;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;RequestException&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;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&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;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="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&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;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;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="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="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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&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;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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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;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;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;Failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&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;def&lt;/span&gt; &lt;span class="nf"&gt;record_result&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="n"&gt;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;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;result&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;entry&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="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;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="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;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;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;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;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;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="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="n"&gt;entry&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="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;duplicate_of&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;  Duplicate: &lt;/span&gt;&lt;span class="si"&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;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; original &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_of&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;  Anchored: &lt;/span&gt;&lt;span class="si"&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;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;name&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;progress&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&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;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;pending&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="nc"&gt;Path&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="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="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="ow"&gt;and&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;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;not&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;Pending: &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;pending&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;def&lt;/span&gt; &lt;span class="nf"&gt;process&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;Path&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;tuple&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;hash_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;path&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_hash&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&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;path&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;result&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MAX_WORKERS&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;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&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;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;pending&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;future&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;future&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;key&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nf"&gt;record_result&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;key&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;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="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="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;  Error: &lt;/span&gt;&lt;span class="si"&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;name&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;e&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;main&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-archive&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;h2&gt;
  
  
  What to Run Next
&lt;/h2&gt;

&lt;p&gt;The progress file is useful beyond crash recovery. Once the batch finishes, parse it to pull all &lt;code&gt;certificate_url&lt;/code&gt; values for a summary report. Filter for &lt;code&gt;duplicate_of&lt;/code&gt; entries to find which files in your archive have identical content.&lt;/p&gt;

&lt;p&gt;For third-party verification, the &lt;code&gt;GET /api/v1/verify?hash=&amp;lt;sha256&amp;gt;&lt;/code&gt; endpoint is public and requires no authentication. Opposing counsel, auditors, or anyone with the original file can independently confirm the anchor. It's rate-limited to 120 requests per hour per IP, which is plenty for verification workflows.&lt;/p&gt;

&lt;p&gt;For offline verification against a saved proof JSON, the &lt;a href="https://pypi.org/project/verify-proof/" rel="noopener noreferrer"&gt;verify-proof&lt;/a&gt; package handles that without any network calls.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>When Evidence Timing Becomes the Dispute</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Tue, 30 Jun 2026 00:00:03 +0000</pubDate>
      <link>https://dev.to/craig_solomon/when-evidence-timing-becomes-the-dispute-17h0</link>
      <guid>https://dev.to/craig_solomon/when-evidence-timing-becomes-the-dispute-17h0</guid>
      <description>&lt;p&gt;A commercial property claim crosses your desk. Water damage to a server room, tenant says it happened during last Tuesday's storm. The property manager provides photos showing the damage, but the timestamps show they were taken three days before the storm hit.&lt;/p&gt;

&lt;p&gt;Now what started as a straightforward water damage claim becomes a timing dispute. Did the damage exist before the storm? Was the tenant aware of a pre-existing leak? The photos exist, but proving when they were captured becomes the central issue.&lt;/p&gt;

&lt;p&gt;This is where blockchain timestamping changes the game. ProofLedger anchors SHA-256 file hashes to both Polygon and Bitcoin blockchains, creating immutable proof that evidence existed at a specific point in time. The original files never leave your device. Only the cryptographic hash gets anchored to the blockchain.&lt;/p&gt;

&lt;p&gt;Here's how the workflow actually works. A property manager documents a site visit by uploading photos to ProofLedger. The system generates a SHA-256 hash of each file and anchors that hash to Polygon for instant confirmation, then to Bitcoin in daily batches with merkle proof verification. &lt;/p&gt;

&lt;p&gt;When the claim becomes disputed months later, the blockchain anchor provides neutral temporal authority. The hash on the blockchain matches the hash of the original file, proving it existed at the recorded timestamp. No party can alter the blockchain record. It would require attacking Bitcoin's network, a multi-billion dollar impossibility, to alter a single timestamp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Court Authentication Under Federal Rules
&lt;/h2&gt;

&lt;p&gt;Courts can authenticate blockchain records under FRE 901(b)(9), which allows authentication of evidence produced by a process that generates an accurate result. The rule requires laying a foundation through expert testimony or written certification, but once established, the blockchain timestamp carries the same weight as any other authenticated record.&lt;/p&gt;

&lt;p&gt;For self-authentication without live testimony, FRE 902(13) and FRE 902(14) cover machine-generated records through written certification. These rules, added in 2017, recognize that reliable automated systems can produce court-ready evidence without requiring the operator to testify in person.&lt;/p&gt;

&lt;p&gt;The key is establishing that the blockchain process itself is reliable. Bitcoin's consensus mechanism and cryptographic security provide exactly that foundation. The network has operated continuously since 2009 without successful attacks on its timestamp integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond Water Damage Claims
&lt;/h2&gt;

&lt;p&gt;Evidence timing disputes appear across the industry. Roofing claims where storm damage gets attributed to pre-existing wear. Construction defect cases where the question becomes whether photos document the original installation or later repairs. Fire investigations where the sequence of documentation affects subrogation recovery.&lt;/p&gt;

&lt;p&gt;A blockchain anchor removes the guesswork. The timestamp becomes a mathematical fact, not a disputable metadata field that can be altered in post-processing.&lt;/p&gt;

&lt;p&gt;The dual-chain approach matters here. Polygon provides instant confirmation for time-sensitive documentation. Bitcoin provides long-term immutability through its proof-of-work security model. Evidence packs organize the proof by claim, case, or matter, with clear pre-loss and post-loss categorization.&lt;/p&gt;

&lt;p&gt;That commercial water damage claim? With blockchain timestamping, the timing question gets resolved in discovery, not at trial. The photos either have valid pre-storm timestamps or they don't. The blockchain record provides the neutral authority both sides can accept.&lt;/p&gt;

&lt;p&gt;Chain of custody just got a mathematical backbone.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>insurance</category>
      <category>legaltech</category>
      <category>evidence</category>
    </item>
    <item>
      <title>Apple v. Samsung: How Institutional Processes Triggered an Adverse Inference</title>
      <dc:creator>Craig Solomon</dc:creator>
      <pubDate>Mon, 29 Jun 2026 23:45:09 +0000</pubDate>
      <link>https://dev.to/craig_solomon/apple-v-samsung-how-institutional-processes-triggered-an-adverse-inference-4id4</link>
      <guid>https://dev.to/craig_solomon/apple-v-samsung-how-institutional-processes-triggered-an-adverse-inference-4id4</guid>
      <description>&lt;h2&gt;
  
  
  The case
&lt;/h2&gt;

&lt;p&gt;Apple Inc. v. Samsung Electronics Co., 888 F. Supp. 2d 976 (N.D. Cal. 2012) was a patent infringement action in which Apple moved for sanctions based on Samsung's failure to preserve relevant evidence. After the duty to preserve attached, Samsung continued operating automatic email deletion routines without implementing adequate litigation holds for employees likely to possess responsive communications. The sanctions motion placed Samsung's organizational response to the hold obligation under scrutiny, not the conduct of any individual custodian.&lt;/p&gt;

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

&lt;p&gt;Judge Lucy Koh imposed an adverse-inference instruction, finding that Samsung's failure to suspend routine deletion policies after the preservation duty attached was sanctionable under the federal common law spoliation framework then in effect. The ruling made clear that systemic institutional failure could generate liability without proof that any individual deliberately destroyed evidence. The 2015 amendment to FRCP 37(e) subsequently raised the bar: courts now require a finding that the party acted with "intent to deprive" before imposing an adverse inference, a threshold the pre-amendment negligence standard Judge Koh applied in 2012 did not require.&lt;/p&gt;

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

&lt;p&gt;The Apple-Samsung sanctions motion illustrates a preservation failure that exists upstream of any individual's choices. Documents weren't deleted because someone decided to delete them. They were deleted because institutional systems kept running on schedule after they should have stopped. The company's own infrastructure became the mechanism of loss.&lt;/p&gt;

&lt;p&gt;A blockchain anchor operates entirely outside those institutional systems. Anchoring a SHA-256 hash to Polygon and Bitcoin creates a record on public, immutable ledgers that no internal deletion routine can reach. A system can remove the underlying file. It cannot alter a transaction already confirmed on two independent chains. The record of the file's existence, and its exact content at the time of anchoring, persists regardless of what any company-side system does next.&lt;/p&gt;

&lt;p&gt;What an anchor proves is bounded and specific. It proves that a file producing that exact hash existed at the moment of anchoring. Content integrity too: any modification produces a completely different hash. What it doesn't prove is that the original file still exists, or that its contents are factually accurate. Those are distinct questions, and a blockchain timestamp doesn't resolve either of them.&lt;/p&gt;

&lt;p&gt;This distinction matters for sanctions analysis. If files were anchored before the deletion routine ran, the hashes on-chain establish that those files existed at a specific point in time with specific content. The files are still gone. But the anchors document the prior existence and the exact state of the content, which is relevant to evaluating prejudice under FRCP 37(e)(1). A party seeking curative measures can point to the on-chain record to establish the scope of what was lost. A party defending against sanctions can demonstrate that the content existed and was preserved in hash form before the deletion occurred.&lt;/p&gt;

&lt;p&gt;Verification doesn't require cooperation from the party whose systems are under examination. The public endpoint at &lt;code&gt;https://proofledger.io/api/v1/verify?hash=&amp;lt;sha256&amp;gt;&lt;/code&gt; returns proof status, chain transaction IDs, and explorer URLs without authentication. For entirely offline verification, the verify-proof Python package performs the same check against on-chain data without contacting any ProofLedger servers. In a case where one party's infrastructure is itself under scrutiny, the ability to verify through a process that neither party controls is directly relevant to reliability under FRE 901(b)(9).&lt;/p&gt;

&lt;p&gt;The dual-chain structure adds corroboration. Polygon provides near-instant finality. Bitcoin's daily batch anchoring uses merkle proofs that let any verifier confirm a specific hash's inclusion in a Bitcoin transaction. Two independent chains, neither controlled by the anchoring party, corroborating the same timestamp: that is the kind of process reliability FRE 901(b)(9) addresses.&lt;/p&gt;

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

&lt;p&gt;Apple v. Samsung established that courts will examine institutional preservation processes, not just individual conduct, when assessing sanctions for ESI loss. Under FRCP 37(e)(1), courts retain authority to impose curative measures proportionate to prejudice even where intent to deprive can't be shown. An anchor doesn't replace a litigation hold or restore a deleted document. What it creates is an independent, verifiable record of prior existence on a public chain that institutional processes cannot retroactively alter. For admissibility, FRE 901(b)(9) covers authentication of evidence from a process or system that generates an accurate result. Where self-authentication without live testimony is available, FRE 902(13) permits written certification of machine-generated records.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Apple Inc. v. Samsung Electronics Co., 888 F. Supp. 2d 976 (N.D. Cal. 2012)&lt;/li&gt;
&lt;li&gt;FRCP 37(e) (2015 amendment): ESI preservation, curative measures, and adverse-inference standard&lt;/li&gt;
&lt;li&gt;FRE 901(b)(9): Authentication of evidence from a process or system producing an accurate result&lt;/li&gt;
&lt;li&gt;FRE 902(13): Self-authentication of machine-generated records via written certification&lt;/li&gt;
&lt;/ul&gt;

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