← All posts
AI systems

The confidence gate: automate the reading, not the deciding

Today

A confidence gate is the rule set that decides whether a language model's extraction is allowed to reach a customer without a human. Automate the reading — classification and extraction — and keep a person on the deciding. Anything below the gate still gets acknowledged, then queued with the reason it was held.

The failure mode

In aviation parts trading, demand arrives as email. A request for quote might be a marketplace notification, three lines of free text, or a scanned PDF of a purchase order. Whoever answers first with a credible price usually wins the order, and the paperwork still has to be perfect at the end.

So the bottleneck is a human reading an inbox and retyping what they find. That is also the error source. The obvious thing to reach for in 2026 is: point a language model at the mailbox and let it answer.

That is the version that gets you in trouble.

Why the obvious fix breaks

A model reading trade email is right most of the time. The business risk lives entirely in the tail.

A misread part number produces a quote for the wrong component. A hallucinated quantity produces a quote you may be expected to honour. And these mistakes do not arrive labelled — they arrive looking exactly like the correct ones, because the model reports high confidence either way. Self-reported confidence is a claim about the model's own state, not a calibrated probability, and it knows nothing about whether the part is actually on your shelf.

There is a second failure that has nothing to do with accuracy. Even a perfectly extracted RFQ can be the wrong thing to answer automatically: at 3am, on the fortieth message of an hour, from a sender the operator has already decided to ignore.

The mistake is framing this as "how accurate does the model have to be before I let it reply?" The useful frame is: the model reads, a rule set decides, and a human owns anything the rule set will not clear.

The mechanism

Four things carry the design.

The acknowledgement is decoupled from the work. Microsoft Graph considers a change notification delivered only if your endpoint returns a 2xx within 3 seconds, and an endpoint whose responses exceed the 10-second retry timeout more than 15% of the time in a 10-minute window gets put into a "drop" state — notifications are discarded for the next 10 minutes. A model call cannot live inside that budget. So the webhook validates, persists and returns 202, and everything else happens on a durable queue. A delta-query poller runs behind it to catch anything the webhook missed.

Cheap filters run before expensive ones. Sender rules are evaluated before any model call, so newsletters and known non-business traffic never cost a token. Suppressed messages are still recorded — "not answered" and "not seen" have to be distinguishable later.

The gate is a rule set, not a threshold. It is per-tenant and it combines the model's confidence with business preconditions:

type GateDecision =
  | { allowed: true }
  | { allowed: false; reason: HeldReason };
 
function evaluateGate(rfq: ParsedRfq, policy: TenantPolicy): GateDecision {
  if (policy.killSwitch) return { allowed: false, reason: "automation_disabled" };
  if (rfq.confidence < policy.minConfidence)
    return { allowed: false, reason: "below_confidence_floor" };
  if (policy.requireInventoryMatch && !rfq.inventoryMatch)
    return { allowed: false, reason: "no_inventory_match" };
  if (policy.requirePriceSource && !rfq.priceSource)
    return { allowed: false, reason: "no_price_source" };
  if (isWithinQuietHours(policy)) return { allowed: false, reason: "quiet_hours" };
  if (repliesSentThisHour(policy) >= policy.hourlyCap)
    return { allowed: false, reason: "hourly_cap_reached" };
 
  return { allowed: true };
}

Note what the reasons are for. They are not log lines — they are the text a human sees at the top of the queued item, and the field you group by when you want to know whether the gate is too tight.

Failing the gate is a route, not a rejection. The counterparty still gets an acknowledgement, because in this business silence loses the order as surely as a wrong price does. The request goes to a person with the parse attached.

The edge cases that shaped it

Scans get escalated, not guessed. Image-only and scanned attachments go to a vision model rather than being run through a text path that would return a confident-looking nothing.

Every parse is a record, not a log line. Model, prompt version, latency and the verbatim output are stored per call and correctable in the UI. This is the part people skip, and it is the part that makes the system operable: when a correction is applied you learn both that the model was wrong and what right looked like.

Replies have to find their way home. When nothing is in stock, the same engine dispatches a sourcing campaign, and supplier answers come back into the same mailbox days later in every conceivable format. Matching runs in layers — marketplace reference, then sender address, then part-number overlap, then fuzzy name — and every match records its confidence, the reason it matched, and a link to the source message. A misfiled reply can be reassigned by hand. An automated system that cannot be corrected by a human is a system that gets switched off.

Discovery compounds only if you write it down. Marketplace searches used to surface suppliers who were then forgotten. Persisting contact data from search results turned that into an asset: in the change that added plain-text email extraction, contact capture on one live results page went from 0 to 13 of 24 suppliers.

The platform this runs in is not small — 86 data models, 215 API routes, 92 screens, 165 test files, multi-tenant from the data layer up, with tenant isolation injected by a scoped database client rather than remembered at each route. But the gate is maybe 40 lines. Placement matters more than size.

Limits, and what I would do differently

I do not claim a model accuracy percentage. The human-labelled ground truth is too sparse to support one. Correction data is accumulating precisely so that an evaluation set can be built from real traffic instead of invented — and until that exists, any accuracy number would be marketing.

Quiet hours and hourly caps are blunt. They protect against the 3am-forty-messages scenario, but the honest version is per-counterparty policy: this customer is fine with automated replies, that one has asked for a human. That is a data model change, not a config change, which is why it has not shipped yet.

The gate can be too tight and look fine. A gate that holds everything produces zero bad replies and zero value. The reason codes exist so that "held" is a distribution you can look at, not a feeling.

If you are shipping anything where a model's output leaves your system and reaches a customer, the gate is the product decision. The model is just the reader.

Frequently asked

What is a confidence gate in an LLM pipeline?
A per-tenant rule set evaluated after extraction and before any outbound action. It combines the model's self-reported confidence with business preconditions — an inventory match, a usable price source, quiet hours, an hourly reply ceiling and a kill switch — and returns either an allow or a machine-readable reason the message was held for a human.
Why not trust the model's own confidence score?
Self-reported confidence is a claim, not a calibrated probability, and it says nothing about the business preconditions of the action. A model can be entirely certain it read a part number correctly and still be wrong about whether you have that part in stock at a price you are willing to honour. The gate treats confidence as one input among several.
What happens to the messages the gate blocks?
They are never silently dropped. The counterparty gets an acknowledgement, and the request lands in a human queue carrying the reason it was held, the source message and the full parse record — which is also how the correction data for a future evaluation set gets collected.

Where this runs in production

FenaviaAI-assisted trading platform for aviation parts