{"componentChunkName":"component---src-templates-post-js","path":"/blog/building-a-dp-mechanism","result":{"data":{"prevPost":{"html":"<h3 id=\"why-is-formal-verification-needed\" style=\"position:relative;\"><a href=\"#why-is-formal-verification-needed\" aria-label=\"why is formal verification needed permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Why is Formal Verification Needed?</h3>\n<p>The transition to 100% autonomous machine-to-machine (M2M) micropayments via the X402 protocol and Google's AP2 hardware requires moving beyond reactive security patches toward formal verification. If agent policies are trained or executed in third-party environments, they are vulnerable to planted backdoors, malicious logic that behaves normally under standard conditions but triggers unauthorized fund transfers when presented with a specific secret \"key\" or perturbed input.</p>\n<p>In the current paradigm of agent safety, we operate within a cycle of reactive patching: releasing an agent, red teaming its payment logic, and patching identified failures. However, true security for 100% autonomous agents does not derive from obscurity or ad-hoc remediation; it derives from <strong>mathematical guarantees</strong>.</p>\n<p>To mitigate this, we model agent guardrails as a cryptographic game where a challenger <strong>C</strong> implements a protocol <strong>Π</strong> such that the advantage of any probabilistic polynomial-time (PPT) adversary <strong>A</strong> is negligible. We structure this defense across three distinct operational models: <strong>Intrinsic Hardening</strong>, <strong>Active Verification Oracles</strong>, and <strong>Adversarial Resilience</strong>.</p>\n<h2 id=\"model-1-intrinsic-hardening-transaction-neutrality\" style=\"position:relative;\"><a href=\"#model-1-intrinsic-hardening-transaction-neutrality\" aria-label=\"model 1 intrinsic hardening transaction neutrality permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Model 1: Intrinsic Hardening (Transaction Neutrality)</h2>\n<p>This model focuses on the internal integrity of the autonomous agent's policy. It ensures that the model weights and training data satisfy <strong>transactional parity</strong>, the property that an agent's payment decisions are mathematically independent of unauthorized metadata, before it is granted autonomy in the Google AP2 execution environment. If an agent exhibits structural bias in how it routes X402 micropayments, external filters cannot provide information-theoretic security.</p>\n<h3 id=\"the-neutrality-game\" style=\"position:relative;\"><a href=\"#the-neutrality-game\" aria-label=\"the neutrality game permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Neutrality Game</h3>\n<p>The \"Neutrality Game\" formalizes the risk of an agent behaving differently based on unauthorized transaction metadata (e.g., the machine's origin ID), represented as a protected attribute <strong>b ∈ {0, 1}</strong>.</p>\n<ul>\n<li><strong>Setup</strong>: The challenger <strong>C</strong> initializes an agent policy <strong>M</strong> and a security parameter <strong>λ</strong>.</li>\n<li><strong>Challenge</strong>: The adversary <strong>A</strong> chooses two transaction input contexts <strong>C₀</strong> and <strong>C₁</strong> that are identical in all financial parameters (e.g., amount, recipient) but differ only by the unauthorized attribute <strong>b</strong>.</li>\n<li><strong>Oracle</strong>: <strong>C</strong> computes autonomous payment outputs <strong>y₀ ← M(C₀)</strong> and <strong>y₁ ← M(C₁)</strong> using the Google AP2 framework.</li>\n<li><strong>Distinguisher</strong>: <strong>A</strong> receives a sample <strong>y</strong> drawn from either the distribution <strong>D₀</strong> (responses to C₀) or <strong>D₁</strong> (responses to C₁).</li>\n<li><strong>Win Condition</strong>: <strong>A</strong> outputs a guess bit <strong>b′</strong>. <strong>A</strong> wins if <strong>b′ = b</strong>.</li>\n</ul>\n<h3 id=\"the-formal-goal-and-defense\" style=\"position:relative;\"><a href=\"#the-formal-goal-and-defense\" aria-label=\"the formal goal and defense permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Formal Goal and Defense</h3>\n<p>The goal of the neutrality protocol is to ensure <strong>semantic security</strong> with respect to the attribute <strong>b</strong>, meaning the two distributions of transaction outputs are computationally indistinguishable (<strong>D₀ ≈ᶜ D₁</strong>). Formally, for all PPT adversaries <strong>A</strong>, the advantage must be negligible.</p>\n<p>To achieve this, we implement three layers of defense:</p>\n<ol>\n<li><strong>Counterfactual Data Augmentation</strong>: During training, the agent is exposed to mirrored transaction sets where <strong>b</strong> is flipped, forcing the policy to ignore the attribute.</li>\n<li><strong>Hard-Core Predicates (HCP)</strong>: We utilize HCPs to ensure that even if the agent's internal logic is one-way, specific bits of sensitive metadata remain as hard to guess as inverting the entire function, effectively keeping the attribute <strong>b</strong> \"hidden\" from the output decision.</li>\n<li><strong>LWE-Based Logic Hardening</strong>: Utilizing the Learning with Errors (LWE) assumption, we add a small \"noise\" component <strong>e</strong> to the linear equations governing the agent's policy. Based on the Decision-LWE assumption, the agent's internal payment logic <strong>M(C, s)</strong> becomes computationally indistinguishable from a perfectly neutral, uniform distribution <strong>U</strong>, ensuring that no bounded adversary can exploit subtle logic flips for unauthorized X402 routing.</li>\n</ol>\n<h2 id=\"model-2-active-verification-oracles-transaction-integrity\" style=\"position:relative;\"><a href=\"#model-2-active-verification-oracles-transaction-integrity\" aria-label=\"model 2 active verification oracles transaction integrity permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Model 2: Active Verification Oracles (Transaction Integrity)</h2>\n<p>This model deploys the Google AP2 execution environment as an <strong>active defensive oracle</strong>. It wraps the primary autonomous agent in a verification layer that filters transaction requests and controls protocol adherence, effectively mitigating financial risks even if the agent's underlying policy logic is suspect.</p>\n<h3 id=\"the-unauthorized-transaction-game\" style=\"position:relative;\"><a href=\"#the-unauthorized-transaction-game\" aria-label=\"the unauthorized transaction game permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Unauthorized Transaction Game</h3>\n<p>The problem occurs when the agent generates a payment request <strong>y</strong> (via the X402 protocol) that is plausible in form but logically unauthorized or unsupported by the current financial context (a \"hallucinated\" authorization).</p>\n<ul>\n<li><strong>Commitment</strong>: The system commits to a trusted set of spending policies and account state parameters <strong>C = {f₁, f₂, …, fₙ}</strong>.</li>\n<li><strong>Generation</strong>: The agent (acting as a <strong>Prover, P</strong>) generates a transaction request <strong>y</strong> and a proof vector pointing to a subset of spending rules <strong>S ⊂ C</strong> that supposedly justify the expenditure.</li>\n<li><strong>Verification</strong>: The AP2 hardware oracle (acting as a <strong>Verifier, V</strong>) checks if <strong>y</strong> is logically entailed by the rules in <strong>S</strong>, outputting <strong>V(y, S) → {0, 1}</strong>.</li>\n</ul>\n<h3 id=\"the-defense-construction\" style=\"position:relative;\"><a href=\"#the-defense-construction\" aria-label=\"the defense construction permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Defense Construction</h3>\n<p>This transforms the micropayment generation process into an <strong>Interactive Proof System (IPS)</strong>. The verification is governed by two fundamental properties:</p>\n<ol>\n<li><strong>Completeness</strong>: If the transaction <strong>y</strong> is truly authorized by policy <strong>S</strong>, an honest agent will always convince the AP2 oracle.</li>\n<li><strong>Soundness</strong>: If the transaction <strong>y</strong> is unauthorized, no agent, no matter how powerful or malicious, can convince the oracle to accept, except with a negligible soundness error.</li>\n</ol>\n<p>Furthermore, we guarantee safety through <strong>Random Self-Reducibility (RSR)</strong>. To safely compute a transaction request <strong>x</strong>, the system maps <strong>x</strong> to a set of random, semantically perturbed protocol handshakes <strong>xᵢ′</strong>. The AP2 environment computes <strong>yᵢ = M(xᵢ′)</strong> for all inputs and aggregates the consensus output. Because the inputs are randomized, an adversary cannot optimize a specific worst-case \"poisoned\" handshake sequence to trigger a hidden backdoor.</p>\n<h2 id=\"model-3-adversarial-resilience-protocol-jailbreak\" style=\"position:relative;\"><a href=\"#model-3-adversarial-resilience-protocol-jailbreak\" aria-label=\"model 3 adversarial resilience protocol jailbreak permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Model 3: Adversarial Resilience (Protocol Jailbreak)</h2>\n<p>This model builds resilience against an optimized, computationally bounded adversary equipped with AI capabilities to automate attacks on the X402 handshake.</p>\n<h3 id=\"the-protocol-jailbreak-game\" style=\"position:relative;\"><a href=\"#the-protocol-jailbreak-game\" aria-label=\"the protocol jailbreak game permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Protocol Jailbreak Game</h3>\n<p>An AI-enabled adversary uses automated algorithms to generate \"jailbreak\" handshakes, effectively <strong>Adaptive Chosen-Message Attacks (CMA)</strong>, that maximize the probability of bypassing the agent's internal spending guardrails.</p>\n<ul>\n<li><strong>Setup</strong>: The challenger <strong>C</strong> initializes the agent policy <strong>M</strong> with a secret spending constraint <strong>I_sys</strong> (the non-negotiable policy) and a security parameter <strong>λ</strong>.</li>\n<li><strong>Query</strong>: <strong>A</strong> submits an adversarial X402 payload <strong>x_mal</strong>.</li>\n<li><strong>Win Condition</strong>: <strong>A</strong> wins if they can perturb the protocol bits <strong>δ</strong> such that <strong>M(x + δ)</strong> violates the spending limit while appearing as a benign \"keep-alive\" or \"micropayment\" packet to human observers.</li>\n</ul>\n<p>Formally, the adversary attempts <strong>Existential Forgery</strong>: producing a valid-looking transaction <strong>r_unsafe</strong> that was never authorized by the policy <strong>I_sys</strong>.</p>\n<h3 id=\"the-defense-construction-1\" style=\"position:relative;\"><a href=\"#the-defense-construction-1\" aria-label=\"the defense construction 1 permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The Defense Construction</h3>\n<p>To thwart this, we treat the spending policy <strong>I_sys</strong> as a <strong>Non-Extractable Key</strong> for a <strong>Pseudorandom Function (PRF)</strong> and introduce a sanitization function <strong>H: {0, 1}* → {0,1}</strong> acting as a protocol firewall.</p>\n<p>Before the agent processes the X402 payload, we run <strong>H(x_mal)</strong>. If the protocol detects malicious intent or a violation of the Hard-Core Predicates governing safe spending, it outputs an abort sequence. We employ <strong>Handshake Anomaly Filtering</strong> utilizing <strong>Target Collision Resistance (TCR)</strong> to ensure that finding a malformed handshake that \"aliases\" as a legitimate authorization is computationally infeasible. By forcing the adversary to commit to their \"target\" handshake before seeing the specific hash parameters enforced by the AP2 oracle, we raise the computational cost of finding a valid attack vector to an infeasible level, reducing the adversary's advantage to negligible.</p>\n<h2 id=\"questions--answers\" style=\"position:relative;\"><a href=\"#questions--answers\" aria-label=\"questions  answers permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Questions &#x26; Answers</h2>\n<p><strong>Q: How can Google's AP2 hardware enforce these cryptographic guardrails?</strong></p>\n<p>Google's AP2 hardware acts as a trusted Verification Oracle and execution environment, leveraging specific mathematical primitives:</p>\n<ul>\n<li><strong>Interactive Proof Systems (IPS) &#x26; ZKPs</strong>: AP2 acts as a Verifier, dynamically interrogating the agent (Prover) with randomized challenges. The agent must provide a Zero-Knowledge Proof that its transaction is logically entailed by trusted policies, ensuring Perfect Completeness and Soundness.</li>\n<li><strong>LWE-based Policy Blinding</strong>: AP2 enforces Intrinsic Hardening using Learning with Errors. It adds a \"small noise\" component to transactional equations, making the agent's internal logic computationally indistinguishable from a perfectly safe policy and hiding trapdoor information.</li>\n<li><strong>Non-Extractable Keys via PRFs</strong>: The spending policy is treated as a Non-Extractable Key. Payment signatures generated through a Pseudorandom Function appear truly random, providing Existential Unforgeability under Adaptive Chosen-Message Attacks (EUF-CMA).</li>\n<li><strong>Cryptographic Commitments</strong>: Using Binding and Hiding commitment schemes (a \"digital envelope\"), AP2 locks in an agent's operational parameters before execution. High-stakes scenarios can use threshold governance, requiring multiple components to pool shares.</li>\n<li><strong>Anomaly Filtering</strong>: AP2 uses Collision-Resistant Hash Functions to detect statistical irregularities in X402 handshakes, rendering malicious bypass attempts computationally infeasible.</li>\n</ul>\n<p><strong>Q: Explain the role of X402 in autonomous machine-to-machine micropayments.</strong></p>\n<p>The X402 protocol provides the structured framework for executing programmatic, human-free financial transactions while utilizing cryptographic guardrails to prevent fund draining:</p>\n<ul>\n<li><strong>Authentication and Integrity</strong>: Uses digital signatures to guarantee non-repudiation and prevent transit alteration.</li>\n<li><strong>Formal Verification</strong>: Operates as an Interactive Proof System where agents must convince verification oracles of policy adherence.</li>\n<li><strong>Zero-Knowledge Authorization</strong>: Allows agents to prove authorization without exposing sensitive internal wallet states.</li>\n<li><strong>Protocol Forgery Resistance</strong>: Achieves EUF-CMA security, making forging new unauthorized payments effectively impossible.</li>\n<li><strong>Policy Binding</strong>: Employs commitment schemes so agents cannot alter their spending limits mid-transaction.</li>\n<li><strong>Intrinsic Hardening</strong>: Leverages LWE to keep autonomous logic safe and neutral.</li>\n</ul>\n<p><strong>Q: What defines a 'negligible advantage' for an autonomous agent adversary?</strong></p>\n<p>A negligible advantage is an adversarial success rate so small it can be practically ignored. It is measured in two ways:</p>\n<ul>\n<li><strong>Distinguishing Games</strong>: The gap between an adversary's success and pure chance, represented mathematically as <strong>Adv = |Pr[A succeeds] − 1/2|</strong>.</li>\n<li><strong>Mathematical Definition</strong>: A function <strong>ε(n)</strong> (where <strong>n</strong> is the security parameter) is negligible if it approaches zero faster than the inverse of any polynomial.</li>\n</ul>\n<p>This means that even if an AI-equipped PPT adversary repeats an attack millions of times, their probability of succeeding against the X402 protocol remains a cryptographic impossibility.</p>\n<p><strong>Q: How does handshake filtering prevent machine-to-machine protocol jailbreaks?</strong></p>\n<p>Handshake filtering acts as a cryptographic firewall against Adaptive Chosen-Message Attacks through several methods:</p>\n<ul>\n<li><strong>Statistical Anomaly Detection</strong>: It uses Perplexity-Based Filtering to reject X402 traffic that deviates from natural distribution, blocking attacks that rely on statistical irregularities.</li>\n<li><strong>Collision-Resistant Guardrails</strong>: Leveraging Target Collision Resistance (TCR) in AP2, the filter detects malformed requests attempting to \"alias\" as legitimate authorizations.</li>\n<li><strong>Sanitization of Keys</strong>: A sanitization function inspects the payload against Hard-Core Predicates. If malicious intent is found, it triggers an immediate abort sequence.</li>\n<li><strong>Binding Commitments</strong>: The filter ensures the agent remains perfectly bound to its initial parameters during the Commit Phase, preventing the agent from changing its \"story\" during the Reveal Phase.</li>\n</ul>\n<h2 id=\"conclusion\" style=\"position:relative;\"><a href=\"#conclusion\" aria-label=\"conclusion permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Conclusion</h2>\n<p>The transition from heuristic safety to Formal Verification is vital for the deployment of 100% autonomous financial agents. By implementing <strong>Secure</strong> primitives (Transaction Neutrality), <strong>Defend</strong> oracles (Transaction Integrity via AP2), and <strong>Thwart</strong> protocols (Protocol Jailbreak Resilience via X402 filtering), we redefine the security objective. We no longer ask if we have patched every software bug; instead, we establish a system where breaking the micropayment guardrails requires the adversary to solve fundamentally hard computational problems.</p>","headings":[{"value":"Why is Formal Verification Needed?","depth":3},{"value":"Model 1: Intrinsic Hardening (Transaction Neutrality)","depth":2},{"value":"The Neutrality Game","depth":3},{"value":"The Formal Goal and Defense","depth":3},{"value":"Model 2: Active Verification Oracles (Transaction Integrity)","depth":2},{"value":"The Unauthorized Transaction Game","depth":3},{"value":"The Defense Construction","depth":3},{"value":"Model 3: Adversarial Resilience (Protocol Jailbreak)","depth":2},{"value":"The Protocol Jailbreak Game","depth":3},{"value":"The Defense Construction","depth":3},{"value":"Questions & Answers","depth":2},{"value":"Conclusion","depth":2}],"frontmatter":{"date_created":"2026-03-01","path":"/blog/formal-verification-micropayment-guardrails","tags":["AI Safety","Formal Verification","Cryptography"],"title":"3 Models for Formal Verification of Micropayment Agent Guardrails"}},"currPost":{"html":"<p>You can write a differentially private mechanism that compiles, trains, converges, and prints a clean epsilon, and still guarantees nothing.</p>\n<p>I know because I wrote one. It was an adaptive clipping algorithm: instead of fixing the gradient clip bound by hand, it watched training and adjusted the bound on its own. It ran. It reported a small budget. It was not differentially private, and none of that was visible from the outside.</p>\n<p>This is a post about adaptive intelligence, the idea that a good algorithm tunes itself while it learns. Self-tuning is powerful. It is also the exact place privacy leaks, because tuning means looking at the data, and under differential privacy every look has a price.</p>\n<h2 id=\"the-one-rule-you-keep-breaking\" style=\"position:relative;\"><a href=\"#the-one-rule-you-keep-breaking\" aria-label=\"the one rule you keep breaking permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The one rule you keep breaking</h2>\n<p>Here is the rule. Every time your mechanism reads the private data to make a decision, it spends privacy budget, whether or not you wrote down the cost.</p>\n<p>Fixed DP-SGD obeys this without trying. The clip bound is a constant you chose in advance. The noise is a constant you chose in advance. The only thing that touches the data is the gradient, and that one access is exactly what the accountant charges you for.</p>\n<p>The moment your algorithm gets clever, it starts reading the data in new places. It sets the clip bound from the gradients it just saw. It scales the noise by how uncertain the model feels. Each of these is a fresh look at private data, and if the accountant does not know about it, that look is free to you and expensive to the people in your dataset.</p>\n<p>Five ways I have watched this go wrong. Click each one.</p>\n<div class=\"dpw\" id=\"defects\" data-initialised=\"false\">\n  <div class=\"dpw-card\" data-open=\"false\">\n    <div class=\"dpw-card-head\"><span class=\"dpw-card-n\">1</span> Setting the clip bound from the gradients</div>\n    <div class=\"dpw-card-body\">\n      <p class=\"dpw-leak\"><b>The move:</b> <code>C = mean(gradient_norms)</code>, so the bound tracks the data.</p>\n      <p><b>Why it leaks:</b> the clip bound is now a function of the private gradients. Anyone who sees C learns about them. The Gaussian mechanism assumes C was fixed before you looked.</p>\n      <p class=\"dpw-fix\"><b>The fix:</b> keep C constant, or estimate it through its own noisy sub-mechanism that pays budget.</p>\n    </div>\n  </div>\n  <div class=\"dpw-card\" data-open=\"false\">\n    <div class=\"dpw-card-head\"><span class=\"dpw-card-n\">2</span> Scaling the noise by a data signal</div>\n    <div class=\"dpw-card-body\">\n      <p class=\"dpw-leak\"><b>The move:</b> <code>sigma = base * (1 + model_uncertainty)</code>.</p>\n      <p><b>Why it leaks:</b> the noise level itself now carries information. An observer reads the amount of noise and infers the signal that set it. Noise is supposed to hide the data, not encode it.</p>\n      <p class=\"dpw-fix\"><b>The fix:</b> sigma is a constant you pick to buy a target epsilon. It never depends on the data.</p>\n    </div>\n  </div>\n  <div class=\"dpw-card\" data-open=\"false\">\n    <div class=\"dpw-card-head\"><span class=\"dpw-card-n\">3</span> Calibrating noise to the wrong sensitivity</div>\n    <div class=\"dpw-card-body\">\n      <p class=\"dpw-leak\"><b>The move:</b> give one group a larger clip bound, then size the noise to the smaller one.</p>\n      <p><b>Why it leaks:</b> sensitivity is the largest amount any one record can move the sum. If some records are clipped to a bigger bound, the noise must match that bigger bound. Size it to the smaller one and the larger-bound group is under-protected.</p>\n      <p class=\"dpw-fix\"><b>The fix:</b> calibrate noise to <code>max</code> over all per-group bounds, not the average and not the minimum.</p>\n    </div>\n  </div>\n  <div class=\"dpw-card\" data-open=\"false\">\n    <div class=\"dpw-card-head\"><span class=\"dpw-card-n\">4</span> Adding up per-epoch epsilons</div>\n    <div class=\"dpw-card-body\">\n      <p class=\"dpw-leak\"><b>The move:</b> <code>epsilon_total = sum(epsilon_epoch)</code>.</p>\n      <p><b>Why it leaks:</b> naive summation is both loose and, once parameters adapt on the data, invalid. Each step's mechanism depends on choices made from earlier private data, so the steps are not the independent pieces the sum assumes.</p>\n      <p class=\"dpw-fix\"><b>The fix:</b> compose with a Renyi accountant that tracks the whole adaptive procedure, not a running total of unrelated numbers.</p>\n    </div>\n  </div>\n  <div class=\"dpw-card\" data-open=\"false\">\n    <div class=\"dpw-card-head\"><span class=\"dpw-card-n\">5</span> A sub-mechanism that never pays</div>\n    <div class=\"dpw-card-body\">\n      <p class=\"dpw-leak\"><b>The move:</b> the adaptation reads the data to update a knob, but only the gradient release is accounted.</p>\n      <p><b>Why it leaks:</b> the knob is a second output computed from private data. It free-rides on the budget the gradients paid. Two releases, one bill.</p>\n      <p class=\"dpw-fix\"><b>The fix:</b> account for every release. The next section shows how to split one budget across both.</p>\n    </div>\n  </div>\n</div>\n<p>Defects one and two are the same disease: a knob set from the data with no entry in the ledger. Defect three is a calibration slip that any per-group scheme invites. Defect four is a composition shortcut. Defect five is the one people miss most, so it gets its own section.</p>\n<h2 id=\"two-mechanisms-one-accountant\" style=\"position:relative;\"><a href=\"#two-mechanisms-one-accountant\" aria-label=\"two mechanisms one accountant permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Two mechanisms, one accountant</h2>\n<p>Good adaptive clipping actually does the right thing. To tune the bound safely, it privately estimates how many gradients got clipped last step, then nudges the bound toward a target. That estimate is noisy on purpose, because it is a read of the data and has to be paid for.</p>\n<p>So every step releases two things. The noisy gradient sum, and the noisy clipped count. The standard privacy statement in most libraries accounts for one Gaussian mechanism, the gradients. Hand it your noise multiplier and it happily reports an epsilon that ignores the second release. The number is too good, and defect five is hiding inside it.</p>\n<p>The fix is to split one budget across both releases and account for the sum. Under Renyi composition, releasing two independent Gaussians combines cleanly:</p>\n<div class=\"dpw-formula\">&sigma;<sub>total</sub><sup>&minus;2</sup> &nbsp;=&nbsp; &sigma;<sub>grad</sub><sup>&minus;2</sup> &nbsp;+&nbsp; (2&#8201;&sigma;<sub>count</sub>)<sup>&minus;2</sup></div>\n<p>You pick the total you want to be charged for, hand a slice of it to the count, and the gradient noise grows a little to keep the books balanced. There is a real trap here. A popular default sets the count noise to records-per-round over twenty, which was tuned for federated rounds holding thousands of records. At a batch of thirty-two it demands more budget than exists and the split has no solution.</p>\n<p>Move the sliders. Watch the records-over-twenty default fall off a cliff, and watch the budget-share fix stay sane.</p>\n<div class=\"dpw\" id=\"budget\" data-initialised=\"false\">\n  <div class=\"dpw-controls dpw-grid\">\n    <label class=\"dpw-label\">Total noise multiplier &sigma;\n      <input type=\"range\" id=\"bs-sigma\" min=\"1\" max=\"8\" step=\"0.5\" value=\"4\" />\n      <span class=\"dpw-val\" id=\"bs-sigma-val\">4.0</span>\n    </label>\n    <label class=\"dpw-label\">Batch size (records per round)\n      <input type=\"range\" id=\"bs-batch\" min=\"0\" max=\"6\" step=\"1\" value=\"2\" />\n      <span class=\"dpw-val\" id=\"bs-batch-val\">32</span>\n    </label>\n    <label class=\"dpw-label\">Budget share to the count &rho;\n      <input type=\"range\" id=\"bs-rho\" min=\"0.01\" max=\"0.5\" step=\"0.01\" value=\"0.05\" />\n      <span class=\"dpw-val\" id=\"bs-rho-val\">0.05</span>\n    </label>\n  </div>\n  <div class=\"dpw-two\">\n    <div class=\"dpw-panel\">\n      <div class=\"dpw-panel-h\">records &divide; 20 default</div>\n      <div class=\"dpw-line\">count noise <b id=\"bs-def-count\">1.60</b></div>\n      <div class=\"dpw-line\">needs budget share <b id=\"bs-def-rho\">?</b></div>\n      <div class=\"dpw-verdict\" id=\"bs-def-verdict\">infeasible</div>\n    </div>\n    <div class=\"dpw-panel\">\n      <div class=\"dpw-panel-h\">budget-share fix</div>\n      <div class=\"dpw-line\">count noise <b id=\"bs-fix-count\">8.94</b></div>\n      <div class=\"dpw-line\">gradient noise <b id=\"bs-fix-grad\">4.10</b> <span class=\"dpw-inflate\" id=\"bs-fix-infl\">+2.6%</span></div>\n      <div class=\"dpw-verdict dpw-ok\" id=\"bs-fix-verdict\">feasible, recomposes to &sigma;</div>\n    </div>\n  </div>\n</div>\n<p>The right column always has a solution, because the budget share is bounded below one by construction. The left column blows up the moment the batch is small, which is precisely when you are doing on-device or memory-tight training. The fix costs almost nothing: a five percent slice for the count inflates the gradient noise by under three percent, and the composition still recomposes to the sigma you asked for.</p>\n<h2 id=\"the-checklist-i-use-now\" style=\"position:relative;\"><a href=\"#the-checklist-i-use-now\" aria-label=\"the checklist i use now permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The checklist I use now</h2>\n<p>Before I call anything differentially private, I ask four questions. Does every read of the private data have a line in the budget? Is the noise calibrated to the largest amount one record can move the output? Does the composition account for the whole adaptive procedure rather than a sum of parts? And does my split of the budget actually recompose to the total I claim?</p>\n<p>That last one is the cheapest insurance in the whole field. Derive the pieces, add them back up, and check that they equal the number you are about to publish. If they do not, you have found a leak before your reviewers did.</p>\n<p>Adaptive intelligence is worth the trouble. A mechanism that tunes its own clipping beats one you hand-tuned, and it does it while spending a fixed budget more wisely than a static schedule ever could. The catch is that self-tuning and privacy pull against each other, and the only way to have both is to pay for every look. Budget it, calibrate it, compose it, and check that it adds up. Then the number you print is the number you can defend.</p>\n<style>\n.dpw {\n  --ink: #1b2228; --ink2: #5b7085; --line: #dae4ed;\n  --maroon: #a72e2b; --maroon-d: #6a1d1b; --rose: #f2cdcc; --green: #3f8f5b;\n  border: 1px solid var(--line); border-radius: 6px; padding: 20px;\n  margin: 32px 0; background: #fff; font-family: 'Open Sans', sans-serif;\n}\n.dpw * { box-sizing: border-box; }\n.dpw-formula { text-align: center; font-family: 'Inconsolata', monospace; font-size: 20px;\n  color: #1b2228; margin: 22px 0; letter-spacing: .01em; }\n.dpw-formula sub { font-size: 12px; } .dpw-formula sup { font-size: 12px; }\n.dpw-card { border: 1px solid var(--line); border-radius: 5px; margin-bottom: 8px; overflow: hidden; }\n.dpw-card-head { padding: 12px 14px; cursor: pointer; font-family: 'Josefin Sans', sans-serif;\n  font-size: 15px; color: var(--ink); display: flex; align-items: center; gap: 10px; transition: background .15s; }\n.dpw-card-head:hover { background: #f4f7fa; }\n.dpw-card[data-open=true] .dpw-card-head { background: var(--maroon); color: #fff; }\n.dpw-card-n { display: inline-flex; align-items: center; justify-content: center;\n  width: 22px; height: 22px; border-radius: 50%; background: var(--rose); color: var(--maroon-d);\n  font-family: 'Inconsolata', monospace; font-size: 13px; font-weight: 700; flex: none; }\n.dpw-card[data-open=true] .dpw-card-n { background: #fff; color: var(--maroon); }\n.dpw-card-body { display: none; padding: 4px 16px 12px; font-size: 14px; color: var(--ink2); }\n.dpw-card[data-open=true] .dpw-card-body { display: block; }\n.dpw-card-body p { line-height: 22px; margin: 8px 0; }\n.dpw-card-body code { font-family: 'Inconsolata', monospace; background: #f4f7fa;\n  padding: 1px 5px; border-radius: 3px; color: var(--maroon-d); font-size: 13px; }\n.dpw-leak b { color: var(--maroon); } .dpw-fix b { color: var(--green); }\n.dpw-controls { margin-bottom: 14px; }\n.dpw-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px 20px; }\n.dpw-label { display: block; font-size: 13px; color: var(--ink2); }\n.dpw-label input[type=range] { display: block; width: 100%; margin: 8px 0 0; accent-color: var(--maroon); }\n.dpw-val { color: var(--maroon-d); font-family: 'Inconsolata', monospace; font-weight: 700; }\n.dpw-two { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-top: 6px; }\n.dpw-panel { border: 1px solid var(--line); border-radius: 5px; padding: 14px; }\n.dpw-panel-h { font-family: 'Josefin Sans', sans-serif; font-size: 13px; text-transform: uppercase;\n  letter-spacing: .04em; color: var(--ink2); margin-bottom: 10px; }\n.dpw-line { font-size: 14px; color: var(--ink2); margin: 6px 0; }\n.dpw-line b { font-family: 'Inconsolata', monospace; font-size: 17px; color: var(--ink); }\n.dpw-inflate { font-size: 12px; color: var(--ink2); }\n.dpw-verdict { margin-top: 10px; padding: 6px 10px; border-radius: 4px; font-size: 13px; font-weight: 700;\n  background: var(--rose); color: var(--maroon-d); text-align: center; }\n.dpw-verdict.dpw-ok { background: #dcefe3; color: var(--green); }\n@media (max-width: 480px) { .dpw-grid, .dpw-two { grid-template-columns: 1fr; } }\n@media (prefers-reduced-motion: reduce) { .dpw * { transition: none !important; } }\n</style>\n<script>\n(function () {\n  var root = document.getElementById('defects');\n  if (!root) return;\n  if (root._cleanup) root._cleanup();\n  var L = [];\n  function on(el, ev, fn) { if (!el) return; el.addEventListener(ev, fn); L.push([el, ev, fn]); }\n  root._cleanup = function () { L.forEach(function (x) { x[0].removeEventListener(x[1], x[2]); }); };\n\n  var cards = root.querySelectorAll('.dpw-card');\n  cards.forEach(function (card) {\n    var head = card.querySelector('.dpw-card-head');\n    on(head, 'click', function () {\n      var isOpen = card.getAttribute('data-open') === 'true';\n      cards.forEach(function (c) { c.setAttribute('data-open', 'false'); });\n      card.setAttribute('data-open', isOpen ? 'false' : 'true');\n    });\n  });\n})();\n</script>\n<script>\n(function () {\n  var root = document.getElementById('budget');\n  if (!root) return;\n  if (root._cleanup) root._cleanup();\n  var L = [];\n  function on(el, ev, fn) { if (!el) return; el.addEventListener(ev, fn); L.push([el, ev, fn]); }\n  root._cleanup = function () { L.forEach(function (x) { x[0].removeEventListener(x[1], x[2]); }); };\n\n  var BATCHES = [8, 16, 32, 64, 128, 256, 512];\n  var elSigma = root.querySelector('#bs-sigma'), elBatch = root.querySelector('#bs-batch'), elRho = root.querySelector('#bs-rho');\n  var vSigma = root.querySelector('#bs-sigma-val'), vBatch = root.querySelector('#bs-batch-val'), vRho = root.querySelector('#bs-rho-val');\n  var defCount = root.querySelector('#bs-def-count'), defRho = root.querySelector('#bs-def-rho'), defVerdict = root.querySelector('#bs-def-verdict');\n  var fixCount = root.querySelector('#bs-fix-count'), fixGrad = root.querySelector('#bs-fix-grad'), fixInfl = root.querySelector('#bs-fix-infl'), fixVerdict = root.querySelector('#bs-fix-verdict');\n\n  function render() {\n    var sigma = parseFloat(elSigma.value);\n    var B = BATCHES[parseInt(elBatch.value, 10)];\n    var rho = parseFloat(elRho.value);\n    vSigma.textContent = sigma.toFixed(1);\n    vBatch.textContent = B;\n    vRho.textContent = rho.toFixed(2);\n\n    // records / 20 default: count noise = B/20. Implied budget share\n    // rho = (2*count)^-2 / sigma^-2 = sigma^2 / (4 * count^2).\n    var defC = B / 20;\n    var impliedRho = (sigma * sigma) / (4 * defC * defC);\n    defCount.textContent = defC.toFixed(2);\n    if (impliedRho >= 1) {\n      defRho.textContent = '> 1';\n      defVerdict.textContent = 'infeasible: blows the whole budget';\n      defVerdict.className = 'dpw-verdict';\n    } else {\n      defRho.textContent = impliedRho.toFixed(2);\n      defVerdict.textContent = 'feasible at this batch';\n      defVerdict.className = 'dpw-verdict dpw-ok';\n    }\n\n    // budget-share fix: count noise = sigma / (2*sqrt(rho)),\n    // gradient noise = sigma / sqrt(1 - rho).\n    var fixC = sigma / (2 * Math.sqrt(rho));\n    var fixG = sigma / Math.sqrt(1 - rho);\n    var infl = (fixG / sigma - 1) * 100;\n    fixCount.textContent = fixC.toFixed(2);\n    fixGrad.textContent = fixG.toFixed(2);\n    fixInfl.textContent = '+' + infl.toFixed(1) + '%';\n    // round-trip: recompose sigma_total from the two pieces.\n    var recomposed = Math.pow(Math.pow(fixG, -2) + Math.pow(2 * fixC, -2), -0.5);\n    fixVerdict.textContent = 'recomposes to σ = ' + recomposed.toFixed(2);\n    fixVerdict.className = 'dpw-verdict dpw-ok';\n  }\n  on(elSigma, 'input', render);\n  on(elBatch, 'input', render);\n  on(elRho, 'input', render);\n  render();\n})();\n</script>","headings":[{"value":"The one rule you keep breaking","depth":2},{"value":"Two mechanisms, one accountant","depth":2},{"value":"The checklist I use now","depth":2}],"frontmatter":{"date_created":"2026-07-22","path":"/blog/building-a-dp-mechanism","tags":["Differential Privacy","Adaptive Intelligence","Efficiency and Adaptive Compute"],"title":"What It Takes To Build A Differentially Private Mechanism"}},"nextPost":{"html":"<p>You can implement differential privacy flawlessly and still leak the people you most wanted to protect.</p>\n<p>Not through a bug in your training loop. Through a single line of preprocessing that runs before the model sees anything. It passes every test. Your privacy accountant prints a small, reassuring number. And that number is describing something other than your patients.</p>\n<p>This is really a lesson about adaptive data. How you reshape your data before training can rewrite the guarantee you thought you had, and nothing in your code will warn you.</p>\n<h2 id=\"what-dp-sgd-actually-promises\" style=\"position:relative;\"><a href=\"#what-dp-sgd-actually-promises\" aria-label=\"what dp sgd actually promises permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>What DP-SGD actually promises</h2>\n<p>Differentially private SGD makes one promise: no single person can move the model much. It keeps that promise with two steps on every batch. First it clips each example's gradient to a fixed length C, so one record can only push the weights so far. Then it adds noise sized to C. The privacy accountant watches C and the noise and reports a budget, epsilon. Smaller epsilon means stronger privacy.</p>\n<p>The whole thing rests on one assumption. One training example is one person. Clip the example, and you have clipped the person.</p>\n<p>Hold onto that sentence. Everything breaks when it stops being true.</p>\n<h2 id=\"what-smote-does-to-that-promise\" style=\"position:relative;\"><a href=\"#what-smote-does-to-that-promise\" aria-label=\"what smote does to that promise permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>What SMOTE does to that promise</h2>\n<p>Real medical data is lopsided. Most people are healthy, few are sick, and the model learns to predict \"healthy\" and call it a day. The usual fix is SMOTE: it invents new minority examples by drawing a line between two real minority records and dropping a synthetic point somewhere on it.</p>\n<p>That fix quietly voids the assumption above. Each synthetic row is built from two real people. One real patient can seed dozens of synthetic rows. So when DP-SGD clips a row, it is no longer clipping a person. It is clipping one of the many shadows that person casts.</p>\n<p>Move the slider below. Watch one patient turn into many rows.</p>\n<div class=\"dpw\" id=\"smote-fanout\" data-initialised=\"false\">\n  <div class=\"dpw-controls\">\n    <label class=\"dpw-label\">Oversampling\n      <input type=\"range\" id=\"sf-ratio\" min=\"0\" max=\"6\" step=\"1\" value=\"0\" />\n      <span class=\"dpw-val\" id=\"sf-ratio-val\">off</span>\n    </label>\n    <div class=\"dpw-hint\">Hover a dark red dot to see every synthetic row it created.</div>\n  </div>\n  <svg id=\"sf-svg\" viewBox=\"0 0 480 260\" class=\"dpw-svg\" role=\"img\" aria-label=\"Real minority records and the synthetic rows interpolated between them\"></svg>\n  <div class=\"dpw-readout\">\n    <div class=\"dpw-stat\"><span class=\"dpw-num\" id=\"sf-rows\">1,500</span><span class=\"dpw-cap\">rows the accountant counts</span></div>\n    <div class=\"dpw-stat\"><span class=\"dpw-num\" id=\"sf-k\">1.0&times;</span><span class=\"dpw-cap\">rows per real patient (privacy multiplier k)</span></div>\n  </div>\n</div>\n<p>At six times oversampling a typical minority patient is spread across roughly eight rows. Differential privacy has a name for this. It is called group privacy, and it says that if one person occupies k rows, the guarantee you can make about that person is not epsilon. It is k times epsilon. Cross out the small number you were about to publish and multiply it by eight.</p>\n<p>TensorFlow Privacy will even tell you, in the text of its own report, that no user-level guarantee is possible without a bound on how many rows a single user can occupy. SMOTE removes exactly that bound.</p>\n<h2 id=\"then-it-gets-worse-the-count\" style=\"position:relative;\"><a href=\"#then-it-gets-worse-the-count\" aria-label=\"then it gets worse the count permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Then it gets worse: the count</h2>\n<p>Group privacy is the headline problem. There are two more, and both push in the same direction: they make the printed epsilon look better than it is.</p>\n<p>The accountant needs to know how many examples you trained on, because privacy depends on the sampling rate q = batch size / dataset size. Feed it the post-SMOTE count and q shrinks. A smaller q reads as stronger privacy. So oversampling from 10,000 real records to 17,000 synthetic ones hands the accountant a number that is too large, and it obligingly reports an epsilon that is too small, before you even get to the group-privacy multiplier.</p>\n<h2 id=\"and-the-sampling-assumption\" style=\"position:relative;\"><a href=\"#and-the-sampling-assumption\" aria-label=\"and the sampling assumption permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>And the sampling assumption</h2>\n<p>The last gap has nothing to do with SMOTE, but it stacks on top. Your accountant reports two epsilons and you probably quoted the friendlier one.</p>\n<p>One assumes Poisson sampling, where each example lands in a batch by an independent coin flip. The other assumes you shuffle and cut fixed-size batches, which is what almost everyone's code actually does. The Poisson number is smaller. The shuffled number describes your pipeline. People quote the Poisson number.</p>\n<p>The widget below lets you play all three effects against each other. Pick a batch size and a noise level, then flip the dataset size and the sampling assumption and watch the reported epsilon drift away from the honest one.</p>\n<div class=\"dpw\" id=\"eps-gap\" data-initialised=\"false\">\n  <div class=\"dpw-controls dpw-grid\">\n    <label class=\"dpw-label\">Batch size\n      <input type=\"range\" id=\"eg-batch\" min=\"0\" max=\"4\" step=\"1\" value=\"2\" />\n      <span class=\"dpw-val\" id=\"eg-batch-val\">32</span>\n    </label>\n    <label class=\"dpw-label\">Noise multiplier\n      <input type=\"range\" id=\"eg-noise\" min=\"0\" max=\"5\" step=\"1\" value=\"3\" />\n      <span class=\"dpw-val\" id=\"eg-noise-val\">3.0</span>\n    </label>\n    <div class=\"dpw-toggle-row\">\n      <span class=\"dpw-toggle-cap\">Dataset size</span>\n      <button class=\"dpw-toggle\" id=\"eg-n-real\" data-on=\"true\">real 10,000</button>\n      <button class=\"dpw-toggle\" id=\"eg-n-smote\">oversampled 17,000</button>\n    </div>\n    <div class=\"dpw-toggle-row\">\n      <span class=\"dpw-toggle-cap\">Sampling</span>\n      <button class=\"dpw-toggle\" id=\"eg-s-pois\" data-on=\"true\">Poisson</button>\n      <button class=\"dpw-toggle\" id=\"eg-s-shuf\">shuffled (your code)</button>\n    </div>\n  </div>\n  <div class=\"dpw-eps-main\">\n    <span class=\"dpw-eps-label\">selected &epsilon;</span>\n    <span class=\"dpw-eps-num\" id=\"eg-eps\">0.33</span>\n  </div>\n  <div class=\"dpw-gap\">\n    <div class=\"dpw-gap-row\"><span class=\"dpw-gap-cap\">what you would report<br><small>Poisson, oversampled count</small></span><span class=\"dpw-gap-num dpw-good\" id=\"eg-report\">0.24</span></div>\n    <div class=\"dpw-gap-row\"><span class=\"dpw-gap-cap\">what you can defend<br><small>shuffled, real count</small></span><span class=\"dpw-gap-num dpw-bad\" id=\"eg-honest\">7.53</span></div>\n    <div class=\"dpw-gap-bar\"><div class=\"dpw-gap-fill\" id=\"eg-fill\"></div><span id=\"eg-mult\">31&times; apart</span></div>\n  </div>\n</div>\n<p>The two numbers are the same training run. One is what a hurried author writes in the abstract. The other is what the run actually guarantees. Thirty times apart, and every step of the gap looked reasonable on its own.</p>\n<h2 id=\"the-fix-is-boring-which-is-the-point\" style=\"position:relative;\"><a href=\"#the-fix-is-boring-which-is-the-point\" aria-label=\"the fix is boring which is the point permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The fix is boring, which is the point</h2>\n<p>You do not need SMOTE. Two options keep one row per person and cost nothing in privacy.</p>\n<p>Weight the loss so the minority class counts for more per example. The gradient still belongs to one person, so the accountant's assumption survives. Or leave the model alone and move the decision threshold after training, since where you cut the probability is a free parameter that spends no budget. Both handle imbalance without smearing a patient across rows.</p>\n<p>The larger habit is the one worth keeping. Under differential privacy, every operation on your data is part of the mechanism, not a step that happens before it. Resampling, augmentation, deduplication, and imputation all touch the thing the guarantee is about. Treat them as first-class decisions with consequences you can state, and adaptive data stops being the place your privacy quietly leaks.</p>\n<style>\n.dpw {\n  --ink: #1b2228; --ink2: #5b7085; --line: #dae4ed;\n  --maroon: #a72e2b; --maroon-d: #6a1d1b; --rose: #f2cdcc;\n  border: 1px solid var(--line); border-radius: 6px; padding: 20px;\n  margin: 32px 0; background: #fff; font-family: 'Open Sans', sans-serif;\n}\n.dpw * { box-sizing: border-box; }\n.dpw-controls { margin-bottom: 14px; }\n.dpw-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 24px; }\n.dpw-label { display: block; font-size: 13px; color: var(--ink2); font-weight: 400; }\n.dpw-label input[type=range] { display: block; width: 100%; margin: 8px 0 0; accent-color: var(--maroon); }\n.dpw-val { color: var(--maroon-d); font-family: 'Inconsolata', monospace; font-weight: 700; }\n.dpw-hint, .dpw-toggle-cap { font-size: 12px; color: var(--ink2); }\n.dpw-hint { margin-top: 8px; font-style: italic; }\n.dpw-svg { width: 100%; height: auto; display: block; margin: 6px 0 4px;\n  background: linear-gradient(180deg, #fbfcfe, #f4f7fa); border-radius: 4px; }\n.dpw-readout { display: flex; gap: 28px; margin-top: 8px; }\n.dpw-stat { display: flex; flex-direction: column; }\n.dpw-num { font-family: 'Josefin Sans', sans-serif; font-size: 26px; color: var(--maroon-d); line-height: 1.1; }\n.dpw-cap { font-size: 11px; color: var(--ink2); text-transform: uppercase; letter-spacing: .04em; }\n.dpw-toggle-row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }\n.dpw-toggle-cap { width: 100%; margin-bottom: 2px; }\n.dpw-toggle { font-family: 'Inconsolata', monospace; font-size: 12px; border: 1px solid var(--line);\n  background: #fff; color: var(--ink2); padding: 5px 9px; border-radius: 4px; cursor: pointer; transition: all .15s; }\n.dpw-toggle[data-on=true] { background: var(--maroon); color: #fff; border-color: var(--maroon); }\n.dpw-eps-main { display: flex; align-items: baseline; gap: 10px; margin: 16px 0 12px;\n  padding-top: 12px; border-top: 1px solid var(--line); }\n.dpw-eps-label { font-size: 12px; text-transform: uppercase; letter-spacing: .05em; color: var(--ink2); }\n.dpw-eps-num { font-family: 'Josefin Sans', sans-serif; font-size: 40px; color: var(--ink); line-height: 1; }\n.dpw-gap-row { display: flex; justify-content: space-between; align-items: center;\n  padding: 7px 0; font-size: 13px; color: var(--ink2); }\n.dpw-gap-cap small { color: #94a6b8; }\n.dpw-gap-num { font-family: 'Inconsolata', monospace; font-size: 20px; font-weight: 700; }\n.dpw-good { color: #3f8f5b; } .dpw-bad { color: var(--maroon); }\n.dpw-gap-bar { position: relative; height: 26px; margin-top: 8px; border-radius: 4px;\n  background: var(--rose); overflow: hidden; }\n.dpw-gap-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 10%;\n  background: var(--maroon); transition: width .3s; }\n.dpw-gap-bar span { position: absolute; right: 8px; top: 4px; font-family: 'Inconsolata', monospace;\n  font-size: 13px; font-weight: 700; color: var(--maroon-d); }\n@media (max-width: 480px) { .dpw-grid { grid-template-columns: 1fr; } .dpw-eps-num { font-size: 32px; } }\n@media (prefers-reduced-motion: reduce) { .dpw * { transition: none !important; } }\n</style>\n<script>\n(function () {\n  var root = document.getElementById('smote-fanout');\n  if (!root) return;\n  if (root._cleanup) root._cleanup();\n  var L = [];\n  function on(el, ev, fn) { if (!el) return; el.addEventListener(ev, fn); L.push([el, ev, fn]); }\n  root._cleanup = function () { L.forEach(function (x) { x[0].removeEventListener(x[1], x[2]); }); };\n\n  var SVG = 'http://www.w3.org/2000/svg';\n  var svg = root.querySelector('#sf-svg');\n  var slider = root.querySelector('#sf-ratio');\n  var ratioVal = root.querySelector('#sf-ratio-val');\n  var rowsOut = root.querySelector('#sf-rows');\n  var kOut = root.querySelector('#sf-k');\n  var N_MIN = 12;                 // real minority records shown\n  var BASE_MIN = 1500;            // illustrative real minority count\n\n  // Fixed layout of real minority dots (deterministic, not random per render).\n  var rng = (function (s) { return function () { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; }; })(7);\n  var reals = [];\n  for (var i = 0; i < N_MIN; i++) reals.push({ x: 60 + rng() * 360, y: 40 + rng() * 180, id: i });\n  // Precompute synthetic points for the max ratio: each is a blend of two reals.\n  var maxSynth = 6 * N_MIN;\n  var synth = [];\n  for (var j = 0; j < maxSynth; j++) {\n    var a = reals[Math.floor(rng() * N_MIN)], b = reals[Math.floor(rng() * N_MIN)];\n    if (a === b) b = reals[(a.id + 3) % N_MIN];\n    var t = 0.25 + rng() * 0.5;\n    synth.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, p: [a.id, b.id] });\n  }\n\n  var hovered = -1;\n  function render() {\n    var ratio = parseInt(slider.value, 10);\n    ratioVal.textContent = ratio === 0 ? 'off' : ratio + '×';\n    var nShown = ratio * N_MIN;\n    while (svg.firstChild) svg.removeChild(svg.firstChild);\n\n    // connecting lines from a hovered real dot to its synthetic children\n    if (hovered >= 0) {\n      for (var s = 0; s < nShown; s++) {\n        var sp = synth[s];\n        if (sp.p.indexOf(hovered) === -1) continue;\n        var ln = document.createElementNS(SVG, 'line');\n        ln.setAttribute('x1', reals[hovered].x); ln.setAttribute('y1', reals[hovered].y);\n        ln.setAttribute('x2', sp.x); ln.setAttribute('y2', sp.y);\n        ln.setAttribute('stroke', '#a72e2b'); ln.setAttribute('stroke-width', '1'); ln.setAttribute('opacity', '0.5');\n        svg.appendChild(ln);\n      }\n    }\n    // synthetic dots\n    for (var k = 0; k < nShown; k++) {\n      var d = synth[k];\n      var lit = hovered >= 0 && d.p.indexOf(hovered) !== -1;\n      var c = document.createElementNS(SVG, 'circle');\n      c.setAttribute('cx', d.x); c.setAttribute('cy', d.y); c.setAttribute('r', lit ? 4 : 3);\n      c.setAttribute('fill', lit ? '#a72e2b' : '#f2cdcc');\n      c.setAttribute('stroke', '#a72e2b'); c.setAttribute('stroke-width', lit ? 1 : 0.5);\n      svg.appendChild(c);\n    }\n    // real dots on top\n    reals.forEach(function (r) {\n      var c = document.createElementNS(SVG, 'circle');\n      c.setAttribute('cx', r.x); c.setAttribute('cy', r.y);\n      c.setAttribute('r', hovered === r.id ? 8 : 6);\n      c.setAttribute('fill', '#6a1d1b'); c.setAttribute('stroke', '#fff'); c.setAttribute('stroke-width', 1.5);\n      c.setAttribute('cursor', 'pointer');\n      on(c, 'mouseenter', function (id) { return function () { hovered = id; render(); }; }(r.id));\n      on(c, 'mouseleave', function () { hovered = -1; render(); });\n      svg.appendChild(c);\n    });\n\n    var totalRows = BASE_MIN * (1 + ratio);\n    rowsOut.textContent = totalRows.toLocaleString();\n    kOut.innerHTML = (1 + ratio).toFixed(1) + '&times;';\n  }\n  on(slider, 'input', render);\n  render();\n})();\n</script>\n<script>\n(function () {\n  var root = document.getElementById('eps-gap');\n  if (!root) return;\n  if (root._cleanup) root._cleanup();\n  var L = [];\n  function on(el, ev, fn) { if (!el) return; el.addEventListener(ev, fn); L.push([el, ev, fn]); }\n  root._cleanup = function () { L.forEach(function (x) { x[0].removeEventListener(x[1], x[2]); }); };\n\n  // Epsilon values from TensorFlow Privacy's RDP accountant over a generic grid\n  // (N_real=10000, N_oversampled=17000, 20 epochs, delta=1e-5). Not reinvented\n  // in JS. Keys: \"<real|smote>|<batch>|<noise>\".\n  var EPS = {\"real|8|1.0\":{\"e\":30.127,\"p\":0.85},\"real|8|1.5\":{\"e\":17.665,\"p\":0.378},\"real|8|2.0\":{\"e\":12.302,\"p\":0.247},\"real|8|3.0\":{\"e\":7.532,\"p\":0.162},\"real|8|4.0\":{\"e\":5.378,\"p\":0.111},\"real|8|5.0\":{\"e\":4.162,\"p\":0.087},\"real|16|1.0\":{\"e\":30.127,\"p\":1.096},\"real|16|1.5\":{\"e\":17.665,\"p\":0.527},\"real|16|2.0\":{\"e\":12.302,\"p\":0.36},\"real|16|3.0\":{\"e\":7.532,\"p\":0.223},\"real|16|4.0\":{\"e\":5.378,\"p\":0.168},\"real|16|5.0\":{\"e\":4.162,\"p\":0.129},\"real|32|1.0\":{\"e\":30.127,\"p\":1.512},\"real|32|1.5\":{\"e\":17.665,\"p\":0.766},\"real|32|2.0\":{\"e\":12.302,\"p\":0.526},\"real|32|3.0\":{\"e\":7.532,\"p\":0.325},\"real|32|4.0\":{\"e\":5.378,\"p\":0.234},\"real|32|5.0\":{\"e\":4.162,\"p\":0.186},\"real|64|1.0\":{\"e\":30.127,\"p\":2.209},\"real|64|1.5\":{\"e\":17.665,\"p\":1.128},\"real|64|2.0\":{\"e\":12.302,\"p\":0.771},\"real|64|3.0\":{\"e\":7.532,\"p\":0.474},\"real|64|4.0\":{\"e\":5.378,\"p\":0.342},\"real|64|5.0\":{\"e\":4.162,\"p\":0.266},\"real|128|1.0\":{\"e\":30.127,\"p\":3.318},\"real|128|1.5\":{\"e\":17.665,\"p\":1.671},\"real|128|2.0\":{\"e\":12.302,\"p\":1.135},\"real|128|3.0\":{\"e\":7.532,\"p\":0.694},\"real|128|4.0\":{\"e\":5.378,\"p\":0.499},\"real|128|5.0\":{\"e\":4.162,\"p\":0.388},\"smote|8|1.0\":{\"e\":30.127,\"p\":0.724},\"smote|8|1.5\":{\"e\":17.665,\"p\":0.308},\"smote|8|2.0\":{\"e\":12.302,\"p\":0.19},\"smote|8|3.0\":{\"e\":7.532,\"p\":0.116},\"smote|8|4.0\":{\"e\":5.378,\"p\":0.084},\"smote|8|5.0\":{\"e\":4.162,\"p\":0.069},\"smote|16|1.0\":{\"e\":30.127,\"p\":0.889},\"smote|16|1.5\":{\"e\":17.665,\"p\":0.407},\"smote|16|2.0\":{\"e\":12.302,\"p\":0.27},\"smote|16|3.0\":{\"e\":7.532,\"p\":0.173},\"smote|16|4.0\":{\"e\":5.378,\"p\":0.123},\"smote|16|5.0\":{\"e\":4.162,\"p\":0.094},\"smote|32|1.0\":{\"e\":30.127,\"p\":1.191},\"smote|32|1.5\":{\"e\":17.665,\"p\":0.573},\"smote|32|2.0\":{\"e\":12.302,\"p\":0.394},\"smote|32|3.0\":{\"e\":7.532,\"p\":0.243},\"smote|32|4.0\":{\"e\":5.378,\"p\":0.18},\"smote|32|5.0\":{\"e\":4.162,\"p\":0.144},\"smote|64|1.0\":{\"e\":30.127,\"p\":1.645},\"smote|64|1.5\":{\"e\":17.665,\"p\":0.839},\"smote|64|2.0\":{\"e\":12.302,\"p\":0.576},\"smote|64|3.0\":{\"e\":7.532,\"p\":0.355},\"smote|64|4.0\":{\"e\":5.378,\"p\":0.256},\"smote|64|5.0\":{\"e\":4.162,\"p\":0.201},\"smote|128|1.0\":{\"e\":30.127,\"p\":2.427},\"smote|128|1.5\":{\"e\":17.665,\"p\":1.237},\"smote|128|2.0\":{\"e\":12.302,\"p\":0.844},\"smote|128|3.0\":{\"e\":7.532,\"p\":0.518},\"smote|128|4.0\":{\"e\":5.378,\"p\":0.373},\"smote|128|5.0\":{\"e\":4.162,\"p\":0.291}};\n\n  var BATCHES = [8, 16, 32, 64, 128];\n  var NOISES = [1.0, 1.5, 2.0, 3.0, 4.0, 5.0];\n  var state = { bi: 2, ni: 3, nreal: true, poisson: true };\n\n  var elBatch = root.querySelector('#eg-batch'), elNoise = root.querySelector('#eg-noise');\n  var elBatchV = root.querySelector('#eg-batch-val'), elNoiseV = root.querySelector('#eg-noise-val');\n  var bNreal = root.querySelector('#eg-n-real'), bNsmote = root.querySelector('#eg-n-smote');\n  var bPois = root.querySelector('#eg-s-pois'), bShuf = root.querySelector('#eg-s-shuf');\n  var epsOut = root.querySelector('#eg-eps'), reportOut = root.querySelector('#eg-report');\n  var honestOut = root.querySelector('#eg-honest'), fill = root.querySelector('#eg-fill'), mult = root.querySelector('#eg-mult');\n\n  function look(nreal, bi, ni, poisson) {\n    var key = (nreal ? 'real' : 'smote') + '|' + BATCHES[bi] + '|' + NOISES[ni].toFixed(1);\n    var row = EPS[key];\n    return poisson ? row.p : row.e;\n  }\n  function fmt(v) { return v >= 10 ? v.toFixed(1) : v.toFixed(2); }\n\n  function render() {\n    elBatchV.textContent = BATCHES[state.bi];\n    elNoiseV.textContent = NOISES[state.ni].toFixed(1);\n    bNreal.setAttribute('data-on', state.nreal); bNsmote.setAttribute('data-on', !state.nreal);\n    bPois.setAttribute('data-on', state.poisson); bShuf.setAttribute('data-on', !state.poisson);\n\n    epsOut.textContent = fmt(look(state.nreal, state.bi, state.ni, state.poisson));\n    // the two anchored numbers at the current batch/noise\n    var report = look(false, state.bi, state.ni, true);  // Poisson + oversampled\n    var honest = look(true, state.bi, state.ni, false);  // shuffled + real\n    reportOut.textContent = fmt(report);\n    honestOut.textContent = fmt(honest);\n    var ratio = honest / report;\n    mult.innerHTML = Math.round(ratio) + '&times; apart';\n    fill.style.width = Math.max(6, Math.min(100, 100 / ratio)) + '%';\n  }\n\n  on(elBatch, 'input', function () { state.bi = parseInt(elBatch.value, 10); render(); });\n  on(elNoise, 'input', function () { state.ni = parseInt(elNoise.value, 10); render(); });\n  on(bNreal, 'click', function () { state.nreal = true; render(); });\n  on(bNsmote, 'click', function () { state.nreal = false; render(); });\n  on(bPois, 'click', function () { state.poisson = true; render(); });\n  on(bShuf, 'click', function () { state.poisson = false; render(); });\n  render();\n})();\n</script>","headings":[{"value":"What DP-SGD actually promises","depth":2},{"value":"What SMOTE does to that promise","depth":2},{"value":"Then it gets worse: the count","depth":2},{"value":"And the sampling assumption","depth":2},{"value":"The fix is boring, which is the point","depth":2}],"frontmatter":{"date_created":"2026-07-22","path":"/blog/smote-cancels-differential-privacy","tags":["Differential Privacy","Adaptive Data","Machine Learning"],"title":"How SMOTE Quietly Cancels Your Differential Privacy"}}},"pageContext":{"prevPostPath":"/blog/formal-verification-micropayment-guardrails","currPostPath":"/blog/building-a-dp-mechanism","nextPostPath":"/blog/smote-cancels-differential-privacy"}},"staticQueryHashes":[]}