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.
the question, and the answer
The question is small. Four nouns and a time.
{
"subject": "user_123",
"asset": "conversation_export",
"purpose": "llm_training",
"actor": "model_pipeline_7",
"requested_at": "2026-06-28T10:20:00Z"
}
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.
{
"allowed": true,
"decision": "allow",
"reason": "active_consent_record_found",
"consent_record_id": "rec_7f3a",
"checked_at": "2026-06-28T10:20:01Z",
"audit_event_id": "audit_91ac"
}
The fields that earn their keep beyond allowed are reason, which is a code you can branch on instead of a sentence you have to parse, and audit_event_id, which points at the stored record of the decision. “We checked” should be something you can show someone, not something you assert.
in code
It comes down to one call and one branch, put where nothing after it runs unless the answer was yes.
const result = await oconsent.verify({
subject: "user_123",
asset: "conversation_export",
purpose: "llm_training",
actor: "model_pipeline_7"
});
if (!result.allowed) {
await skipAsset(result.reason);
} else {
await runPipeline();
}
Handle the deny path deliberately, with the reason, rather than letting it slip through. And if verify 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.
the reason codes
Branching on a fixed set of codes is what stops every caller from parsing prose. Allows come back with one of active_consent_record_found, purpose_allowed, actor_allowed, or scope_valid. Denials carry no_consent_record_found, consent_expired, consent_revoked, purpose_not_allowed, actor_not_allowed, or scope_violation. The gap between no_consent_record_found and consent_revoked 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.
where the check goes
The same call sits in front of every place data crosses into AI use: a fine-tuning job reading a row (fine_tuning_pipeline), an agent writing a fact to memory (agent_memory_store), a retrieval step hitting a vector index (vector_search_gateway), a dataset export (dataset_export_job), a handoff to a partner (data_sharing_api), an evaluation run against real data (evaluation_harness). Naming the spot lets the audit trail later say not just what was decided but where.
doing it for real
A dataset is not one check. It is a few million. Two things are worth knowing. Group the checks by (subject, purpose, actor) 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.
The request and response objects are in the draft spec, and there are full flows on the examples page.
Written by Subhadip Mitra. Found a mistake or want to push back? Open an issue or email [email protected].