<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://oconsent.io/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://oconsent.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-06-28T15:19:44+00:00</updated><id>https://oconsent.io/blog/feed.xml</id><title type="html">oConsent</title><subtitle>oConsent is an open protocol for recording consent so anyone can check it later, without trusting the company that collected it. Signed, timestamped, and anchored on a public chain. Backed by a 2022 paper, with a working reference implementation in Python and Solidity.</subtitle><author><name>Subhadip Mitra</name></author><entry><title type="html">Revocation and audit: making AI data use provable</title><link href="https://oconsent.io/blog/revocation-and-audit/" rel="alternate" type="text/html" title="Revocation and audit: making AI data use provable" /><published>2026-04-22T00:00:00+00:00</published><updated>2026-04-22T00:00:00+00:00</updated><id>https://oconsent.io/blog/revocation-and-audit</id><content type="html" xml:base="https://oconsent.io/blog/revocation-and-audit/"><![CDATA[<p>Handing out consent is the easy bit, and it is not the part that worries me. Taking it back so the change reaches systems already using the data, and being able to say afterward exactly what got used and on whose say-so, those are the two that do. Most consent stories skip both of them. Here is how OConsent deals with each.</p>

<h2 id="revoking-is-something-you-record-not-something-you-erase">revoking is something you record, not something you erase</h2>

<p>When a subject pulls their consent, we do not delete the record. We write a revocation event and flip the record’s status.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rev_22b9"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"consent_record_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rec_7f3a"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"subject"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user_123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"revoked_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-07-10T09:00:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user_requested_revocation"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The next time anything verifies against that record, it gets a no.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"allowed"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"decision"</span><span class="p">:</span><span class="w"> </span><span class="s2">"deny"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"consent_revoked"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"consent_record_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rec_7f3a"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Deleting the record would feel tidier and would be a mistake. Keep it, and you can still answer two separate questions honestly: was this allowed when it happened, and is it allowed now. Throw it away and you have lost the first answer to clean up the second.</p>

<h2 id="the-part-nobody-likes-to-say-out-loud">the part nobody likes to say out loud</h2>

<p>Revocation cannot reach into a model whose weights already learned the data. It cannot pull back a copy a partner already holds, or one sitting in a cache somewhere. You cannot un-train a model, and any consent product that implies you can is selling something. What revocation can do is make every future check fail and leave a dated record that the cleanup obligations downstream can hang off.</p>

<p>That is the real reason to check at the point of use. If collection was the only place you ever enforced anything, a revocation has nothing to act on. Enforce at use, and a withdrawal takes hold the next time a check runs. The short cache lifetimes from the last post are just how you keep the window between “withdrawn” and “actually stopped” small.</p>

<h2 id="audit-is-what-makes-any-of-this-provable">audit is what makes any of this provable</h2>

<p>Every verification can write an audit event. This is the thing the verification response was pointing at.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"audit_91ac"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"consent_record_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rec_7f3a"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"actor"</span><span class="p">:</span><span class="w"> </span><span class="s2">"model_pipeline_7"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"asset"</span><span class="p">:</span><span class="w"> </span><span class="s2">"conversation_export"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"purpose"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llm_training"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"decision"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allow"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"checked_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-06-28T10:20:01Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"enforcement_point"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fine_tuning_pipeline"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>One object that says who used what, for which purpose, under which record, what was decided, and where it happened. The reason it beats a folder of screenshots is that the question you actually get asked is never “do you have a policy.” It is “show me the decision for this asset, on this date, by this pipeline,” and a policy document has no answer to that.</p>

<h2 id="evidence-you-can-edit-is-not-evidence">evidence you can edit is not evidence</h2>

<p>An audit log you can quietly rewrite later is just a better-looking story. The floor is append-only: events get written, never updated, ordered by time. If you also need to defend the log itself, optional cryptographic anchoring chains each event to the one before it and pins the chain to an outside timestamp, so a missing or altered event shows up rather than slipping by. This is where the older protocol work, the signatures and the anchoring, actually pulls its weight: sitting under the audit trail as proof, not on the front page as a slogan.</p>

<h2 id="making-the-trail-something-people-use">making the trail something people use</h2>

<p>Write an event for every decision, denials included, since the denials are usually the ones you end up needing to prove you made. And keep the export boring and stable, the kind of thing a legal or security team can pull on their own, because an audit trail that needs an engineer and a one-off query is a trail nobody ever checks.</p>

<p>The revocation and audit objects are specified in the <a href="/spec/">draft spec</a>, and the <a href="/examples/#agent-memory-permission">agent memory example</a> runs through a grant, a use, a revocation, and the denial that comes after.</p>]]></content><author><name>Subhadip Mitra</name></author><summary type="html"><![CDATA[Withdrawing consent so it actually propagates, and producing evidence of every decision that you cannot quietly edit later.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://oconsent.io/assets/img/og-card.png" /><media:content medium="image" url="https://oconsent.io/assets/img/og-card.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Point-of-use verification: gating AI workloads on consent</title><link href="https://oconsent.io/blog/point-of-use-verification/" rel="alternate" type="text/html" title="Point-of-use verification: gating AI workloads on consent" /><published>2025-11-04T00:00:00+00:00</published><updated>2025-11-04T00:00:00+00:00</updated><id>https://oconsent.io/blog/point-of-use-verification</id><content type="html" xml:base="https://oconsent.io/blog/point-of-use-verification/"><![CDATA[<p>The mistake I keep running into is consent that gets checked once, when it is collected, and then never again. By the time a model actually reaches for the data, months later, nobody is asking whether that particular use is still allowed. OConsent moves the check to the point of use: the line right before a dataset, an agent, or a pipeline touches an asset. It is the one place where you know who is asking, what for, and whether the record still holds, all at once.</p>

<h2 id="the-question-and-the-answer">the question, and the answer</h2>

<p>The question is small. Four nouns and a time.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"subject"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user_123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"asset"</span><span class="p">:</span><span class="w"> </span><span class="s2">"conversation_export"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"purpose"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llm_training"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"actor"</span><span class="p">:</span><span class="w"> </span><span class="s2">"model_pipeline_7"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"requested_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-06-28T10:20:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The answer carries more than a yes or no, because a bare boolean is no help an hour later when something has gone wrong and you are trying to work out why a row got skipped.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"allowed"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"decision"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allow"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"active_consent_record_found"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"consent_record_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rec_7f3a"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"checked_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-06-28T10:20:01Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"audit_event_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"audit_91ac"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The fields that earn their keep beyond <code class="language-plaintext highlighter-rouge">allowed</code> are <code class="language-plaintext highlighter-rouge">reason</code>, which is a code you can branch on instead of a sentence you have to parse, and <code class="language-plaintext highlighter-rouge">audit_event_id</code>, which points at the stored record of the decision. “We checked” should be something you can show someone, not something you assert.</p>

<h2 id="in-code">in code</h2>

<p>It comes down to one call and one branch, put where nothing after it runs unless the answer was yes.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">oconsent</span><span class="p">.</span><span class="nf">verify</span><span class="p">({</span>
  <span class="na">subject</span><span class="p">:</span> <span class="dl">"</span><span class="s2">user_123</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">asset</span><span class="p">:</span> <span class="dl">"</span><span class="s2">conversation_export</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">purpose</span><span class="p">:</span> <span class="dl">"</span><span class="s2">llm_training</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">actor</span><span class="p">:</span> <span class="dl">"</span><span class="s2">model_pipeline_7</span><span class="dl">"</span>
<span class="p">});</span>

<span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">result</span><span class="p">.</span><span class="nx">allowed</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nf">skipAsset</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">reason</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="k">await</span> <span class="nf">runPipeline</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Handle the deny path deliberately, with the reason, rather than letting it slip through. And if <code class="language-plaintext highlighter-rouge">verify</code> throws or times out, treat that as a deny as well. A check that fails open is not really a check, it is a formality you are paying for.</p>

<h2 id="the-reason-codes">the reason codes</h2>

<p>Branching on a fixed set of codes is what stops every caller from parsing prose. Allows come back with one of <code class="language-plaintext highlighter-rouge">active_consent_record_found</code>, <code class="language-plaintext highlighter-rouge">purpose_allowed</code>, <code class="language-plaintext highlighter-rouge">actor_allowed</code>, or <code class="language-plaintext highlighter-rouge">scope_valid</code>. Denials carry <code class="language-plaintext highlighter-rouge">no_consent_record_found</code>, <code class="language-plaintext highlighter-rouge">consent_expired</code>, <code class="language-plaintext highlighter-rouge">consent_revoked</code>, <code class="language-plaintext highlighter-rouge">purpose_not_allowed</code>, <code class="language-plaintext highlighter-rouge">actor_not_allowed</code>, or <code class="language-plaintext highlighter-rouge">scope_violation</code>. The gap between <code class="language-plaintext highlighter-rouge">no_consent_record_found</code> and <code class="language-plaintext highlighter-rouge">consent_revoked</code> is the gap between never having had permission and having had it and lost it, and your logs should be able to tell those apart on their own.</p>

<h2 id="where-the-check-goes">where the check goes</h2>

<p>The same call sits in front of every place data crosses into AI use: a fine-tuning job reading a row (<code class="language-plaintext highlighter-rouge">fine_tuning_pipeline</code>), an agent writing a fact to memory (<code class="language-plaintext highlighter-rouge">agent_memory_store</code>), a retrieval step hitting a vector index (<code class="language-plaintext highlighter-rouge">vector_search_gateway</code>), a dataset export (<code class="language-plaintext highlighter-rouge">dataset_export_job</code>), a handoff to a partner (<code class="language-plaintext highlighter-rouge">data_sharing_api</code>), an evaluation run against real data (<code class="language-plaintext highlighter-rouge">evaluation_harness</code>). Naming the spot lets the audit trail later say not just what was decided but where.</p>

<h2 id="doing-it-for-real">doing it for real</h2>

<p>A dataset is not one check. It is a few million. Two things are worth knowing. Group the checks by <code class="language-plaintext highlighter-rouge">(subject, purpose, actor)</code> and most of those millions collapse into a handful of real decisions. And think twice before caching a result, because a cached allow is only as current as your appetite for missing a revocation. Keep the lifetime short and let revocation win the tie. That last point is where this stops being simple, and it is what I want to write about next.</p>

<p>The request and response objects are in the <a href="/spec/">draft spec</a>, and there are full flows on the <a href="/examples/">examples</a> page.</p>]]></content><author><name>Subhadip Mitra</name></author><summary type="html"><![CDATA[How to turn a consent record into an allow or deny decision at the moment data is about to be used.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://oconsent.io/assets/img/og-card.png" /><media:content medium="image" url="https://oconsent.io/assets/img/og-card.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The consent record: a machine-readable schema for AI data use</title><link href="https://oconsent.io/blog/machine-readable-consent-record/" rel="alternate" type="text/html" title="The consent record: a machine-readable schema for AI data use" /><published>2025-06-17T00:00:00+00:00</published><updated>2025-06-17T00:00:00+00:00</updated><id>https://oconsent.io/blog/machine-readable-consent-record</id><content type="html" xml:base="https://oconsent.io/blog/machine-readable-consent-record/"><![CDATA[<p>We argued about the shape of this object longer than almost anything else in OConsent, and that was the right place to spend the time. Everything in the system either writes one of these or reads one, so a mistake here shows up everywhere later.</p>

<p>The job it has to do is narrow. When a model or a pipeline is about to touch some data, something has to decide whether that is allowed, in code, with nobody around to ask. So the record has to stand on its own and answer one question: can this actor use this asset for this purpose, today.</p>

<p>Here is one in full.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rec_7f3a"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"subject"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user_123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"asset"</span><span class="p">:</span><span class="w"> </span><span class="s2">"conversation_export"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"purpose"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llm_training"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"actor"</span><span class="p">:</span><span class="w"> </span><span class="s2">"model_pipeline_7"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"allowed_operations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"train"</span><span class="p">,</span><span class="w"> </span><span class="s2">"evaluate"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"excluded_operations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"resell"</span><span class="p">,</span><span class="w"> </span><span class="s2">"share_external"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"geography"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"SG"</span><span class="p">,</span><span class="w"> </span><span class="s2">"US"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"retention_days"</span><span class="p">:</span><span class="w"> </span><span class="mi">365</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"issued_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-06-28T00:00:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2027-06-28T00:00:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"active"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"proof"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"signed_timestamp"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sha256:..."</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Let me walk through it.</p>

<h2 id="subject-asset-actor">subject, asset, actor</h2>

<p><code class="language-plaintext highlighter-rouge">subject</code> is who the data is about. <code class="language-plaintext highlighter-rouge">asset</code> is the thing being used. <code class="language-plaintext highlighter-rouge">actor</code> is who wants to use it. All three are required, and that is on purpose: if you cannot name all of them, you do not really have a decision to make. Name the asset at whatever level you can actually enforce on. If you only ever store coarse dataset names, you will regret it the first time someone asks to revoke a single conversation instead of the whole set, so the field goes as fine-grained as you need.</p>

<h2 id="purpose">purpose</h2>

<p>This one looks trivial and is not.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"purpose"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llm_training"</span><span class="w">
</span></code></pre></div></div>

<p>It is a string from a known list, never free text, and that limitation is the reason a machine can use the record at all. The common values are <code class="language-plaintext highlighter-rouge">llm_training</code>, <code class="language-plaintext highlighter-rouge">model_finetuning</code>, <code class="language-plaintext highlighter-rouge">agent_memory</code>, <code class="language-plaintext highlighter-rouge">personalization</code>, <code class="language-plaintext highlighter-rouge">evaluation</code>, <code class="language-plaintext highlighter-rouge">research</code>, and <code class="language-plaintext highlighter-rouge">partner_sharing</code>, and you can add your own. The one rule is that you do not rename them later. A purpose name is a contract, so renaming it quietly invalidates every record that used the old one, which is the kind of thing you do exactly once.</p>

<h2 id="scope">scope</h2>

<p>Purpose says what for. Scope draws the edges.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nl">"allowed_operations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"train"</span><span class="p">,</span><span class="w"> </span><span class="s2">"evaluate"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"excluded_operations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"resell"</span><span class="p">,</span><span class="w"> </span><span class="s2">"share_external"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"geography"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"SG"</span><span class="p">,</span><span class="w"> </span><span class="s2">"US"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"retention_days"</span><span class="p">:</span><span class="w"> </span><span class="mi">365</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">allowed_operations</code> is the actual grant. Anything not on that list is denied, so leaving something off is a choice rather than an oversight. <code class="language-plaintext highlighter-rouge">excluded_operations</code> is nearly redundant against that, and we kept it anyway, because someone reading their own record should see “train, fine; resell, never” written down instead of having to infer it. <code class="language-plaintext highlighter-rouge">geography</code> and <code class="language-plaintext highlighter-rouge">retention_days</code> are the dull constraints that turn out to carry most of the weight in any real conversation with a legal team.</p>

<h2 id="status">status</h2>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"active"</span><span class="w">
</span></code></pre></div></div>

<p>The values are <code class="language-plaintext highlighter-rouge">active</code>, <code class="language-plaintext highlighter-rouge">expired</code>, <code class="language-plaintext highlighter-rouge">revoked</code>, and <code class="language-plaintext highlighter-rouge">suspended</code>. Verification reads this almost first, because most denials in practice are not “no record exists.” They are “a record exists and it is no good now.” Dead records stay; we do not delete them. That is what lets you say later whether a use was fine at the time even if it would be refused today.</p>

<h2 id="proof">proof</h2>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"proof"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"signed_timestamp"</span><span class="p">,</span><span class="w"> </span><span class="nl">"hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sha256:..."</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>A signature over the record and a timestamp that does not lean on the collector’s own clock. If you need to defend the record harder than that, there is optional cryptographic anchoring underneath, but most people will not, and I would rather it sit there as an option than crowd out the description of what a record is.</p>

<h2 id="why-it-ended-up-like-this">why it ended up like this</h2>

<p>Three things we would not bend on. The record is tied to a purpose, so a grant for evaluation cannot wander into a grant for training. It is self-contained, so a verifier can decide from the record plus the request without phoning home to wherever consent was first collected. And it maps onto LLMConsent-style records deliberately, so the same object can travel between systems that were never built to talk to each other.</p>

<p>The rest of the object model is in the <a href="/spec/">draft spec</a>, and the <a href="/developers/">developer guide</a> shows how to issue and check one in code.</p>]]></content><author><name>Subhadip Mitra</name></author><summary type="html"><![CDATA[A field-by-field look at the consent record OConsent issues, and why each part is in there.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://oconsent.io/assets/img/og-card.png" /><media:content medium="image" url="https://oconsent.io/assets/img/og-card.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Consent should be an instrument, not a checkbox</title><link href="https://oconsent.io/blog/consent-is-an-instrument/" rel="alternate" type="text/html" title="Consent should be an instrument, not a checkbox" /><published>2025-01-20T00:00:00+00:00</published><updated>2025-01-20T00:00:00+00:00</updated><id>https://oconsent.io/blog/consent-is-an-instrument</id><content type="html" xml:base="https://oconsent.io/blog/consent-is-an-instrument/"><![CDATA[<p>Think about how the world handles agreements that actually matter. A deed to a house. A share in a company. A passport. None of them is a checkbox on someone’s server. Each is an instrument: signed, sealed, dated, and built so that the party who holds it cannot quietly change what it says. You can take it to a stranger and they can verify it without phoning the person who issued it.</p>

<p>Consent to use your data is treated as none of those things. You click “I agree,” and the only record of what you agreed to lives with the company that benefits from the agreement, in a format they alone control. They can lose it, reword it, or read it generously in their own favour. When a regulator asks for proof, they produce a log they wrote about themselves. That is not evidence. It is a story.</p>

<p>oConsent treats a yes the way the world treats a deed.</p>

<h2 id="what-that-means-in-practice">What that means in practice</h2>

<p>A consent record names who the data is about, who may process it, for exactly which purposes, and for how long. The subject signs it with their own key, so it cannot be forged in their name or edited after the fact without the signature breaking. An independent timestamp, drawn from the NIST Beacon and from Bitcoin, fixes the date beyond the collector’s word. The proof is entered on a public chain, while the detail stays off it. From then on, anyone holding the record can verify it is real, current, and valid for a given use, without anyone’s permission. Revocation is entered the same provable way, so “I took it back” is as solid as “I agreed.”</p>

<p>That whole lifecycle is not a sketch. It is written, tested, and runnable today, in a Python SDK, a command line, and Solidity contracts you can read end to end. You can <a href="/how-it-works/">issue and verify a record</a> this afternoon.</p>

<h2 id="what-is-still-ahead">What is still ahead</h2>

<p>The honest frontier is short and specific. The records carry a hash commitment now, where the design calls for full zero-knowledge proofs, so a processor could demonstrate valid consent without revealing the underlying detail. That is the next major build. Ethereum is wired; other chains are open ground. There is no hosted product, by choice, because right now the protocol is the product. All of that lives on the <a href="/status/">ledger</a>, stated plainly, because a project about proof has no business asking you to take its word.</p>

<p>One small thing, since it is the whole point: this site sets no cookies, runs no third-party trackers, and serves its own fonts. Reading this does not feed you into anyone’s analytics. That should be the floor for a privacy project, not a feature.</p>

<h2 id="come-build-it">Come build it</h2>

<p>oConsent is small and open on purpose. The most useful thing you can do is read the protocol and try to forge a record it would accept. After that the open work is concrete: the zero-knowledge proofs, more chains, sharper docs. The <a href="https://github.com/bassrehab/oconsent">code is here</a>, and <a href="/contribute/">contributing</a> tells you where to start. If you would rather just watch it grow, the <a href="/blog/feed.xml">feed</a> is the no-tracking way to do it.</p>]]></content><author><name>Subhadip Mitra</name></author><summary type="html"><![CDATA[Why oConsent treats a yes the way the world treats a deed: signed, sealed, timestamped, and impossible to quietly rewrite.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://oconsent.io/assets/img/og-card.png" /><media:content medium="image" url="https://oconsent.io/assets/img/og-card.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>