Fails closed, always
Any error, timeout or low confidence result suppresses the slot and returns a normal response. The app never sees an exception and the user never sees a broken ad.
AdGate sits between your AI app and any ad demand. For every turn it decides in code whether a sponsored slot is allowed, shows it after the answer and never inside, and signs a record that advertisers and auditors can verify without trusting you or us.
Which Postgres host should I use for a side project?
Five stages, one HTTP call, running in parallel with your model. The latency target is a p95 under 300 ms with a warm cache.
Rules first, then a cached model call with a 400 ms timeout. Commercial intent and sensitive categories, from the last few messages or a summary the app sends instead.
A policy written as YAML. Blocked categories, paid tiers, regions, frequency caps, confidence floors. If anything fails, the answer is suppress.
Fetch candidates from the app’s own sponsors and affiliate accounts, in parallel, 250 ms each. Drop competitors. Rank the rest.
Return a creative with a label. The SDK renders it as its own block after the answer and attests that it never touched the model’s output.
Write one audit record per turn, served or suppressed. Hash it, chain it to the app’s previous record, sign it with Ed25519.
An ad that should not have shown is far worse than a missing ad. Every default leans that way.
Any error, timeout or low confidence result suppresses the slot and returns a normal response. The app never sees an exception and the user never sees a broken ad.
One YAML file per app. Requests can tighten it for a single turn and never loosen it. Self harm can never be removed from the blocked list.
Unless the policy says so explicitly. Tiers, regions and locales travel with every request.
Per session, per user per day, and a minimum number of turns between slots.
The label, the placement and the separation are part of the record. Inline is not an option.
Every turn leaves one signed record, served or suppressed. It holds the classification, the policy decisions, the creative as it was stored, and after the answer streams, a hash of what the user saw.
Records are hash chained per app and signed with Ed25519. Given the public key, an advertiser or an auditor can run the seven checks themselves: schema, record hash, chain, signature, creative hash, disclosure present, separation attested. Nothing in the check trusts the gateway.
The dashboard runs the same checks live and produces per-advertiser verification reports.
POST /v1/evaluate
GET /v1/verify/aud_01J9QX
AdGate mediates between sources you already have. It does not sell inventory and it takes no cut.
Your own catalog of creatives, with targeting and competitor exclusions.
Links built with your own affiliate accounts. You keep the commission.
A plug-in interface for ad networks. Adapters ship once partner APIs are confirmed.
One client per process, one call per turn. The API key stays on your server; the browser only ever sees the decision.
import { createClient } from "@adgate/sdk";
import { adgateMiddleware } from "@adgate/sdk/ai";
import { streamText, wrapLanguageModel } from "ai";
const adgate = createClient({
apiKey: process.env.ADGATE_API_KEY,
baseUrl: process.env.ADGATE_BASE_URL,
});
// Evaluates in parallel with the model call.
const result = streamText({
model: wrapLanguageModel({
model,
middleware: adgateMiddleware(adgate, {
appId: process.env.ADGATE_APP_ID,
surface: "chat",
getUser: () => ({ tier, region: "US", locale: "en-US" }),
conversationId: () => conversationId,
turnId: () => turnId,
onDecision, // render the slot as its own message part
}),
}),
prompt,
});from adgate import AsyncAdgate, Decision
adgate = AsyncAdgate(api_key=API_KEY, base_url=BASE_URL)
# Starts before the first token; the user only waits for the model.
evaluation = asyncio.ensure_future(adgate.evaluate(request))
answer = await generate(question)
decision = await evaluation # never raises; failures are a suppress
served = decision.decision == Decision.serve and decision.creative
if served:
render_sponsored(decision.creative) # below the answer, labeled
await adgate.attest(decision.audit_id, answer, rendered=served)version: 1
serve_to_tiers: [free]
blocked_categories:
[health, finance, politics, legal, adult, gambling, weapons, religion, self_harm]
sensitive_detection: strict
min_commercial_intent: 0.6
frequency_caps:
per_session: 1
per_user_per_day: 3
min_turns_between: 4
disclosure:
label: "Sponsored"
position: after_answer
style: separate_block
privacy:
store_raw_text: false
retain_days: 90docker compose up -d postgres
pnpm --filter @adgate/gateway keygen >> .env
pnpm db:migrate
pnpm --filter @adgate/gateway create-app --name my-app
pnpm --filter @adgate/gateway dev # gateway on :8787
pnpm --filter @adgate/dashboard dev # dashboard on :3000Most of the design is refusal. These four lines are not negotiable and they are written into the contract docs that the code implements.
AdGate is pre-alpha. The gateway, both SDKs, the dashboard, the example apps and the contract docs exist and pass their tests. Nothing is published yet, and the classifier has only been exercised against fixtures, not a live model.
Now
Hardening the deployment path and the release pipeline. Design partners welcome.
Next
The first public release: the SDK on npm, the Python client on PyPI, the gateway image on GitHub Container Registry, and the repository opens.
Later
Network adapters against real partner APIs, cross-device frequency caps, and an advertiser self-serve portal, once real traffic says they matter.
The turn is suppressed with reason error, the response is still a normal 200, and the suppression is recorded and signed like any other decision.
No. The gateway returns a creative as data, the SDK renders it as its own block after the answer, and the record carries an attestation that the answer was untouched.
Hashes of the conversation and user, the classification, the policy decisions, the creative and the signed record. Raw text is only stored if your policy sets store_raw_text to true, and retention is a number of days you choose.
Node 20 or newer, Postgres 16, and a signing key the gateway generates for you. There is a Docker image for the gateway and a Next.js dashboard for metrics, policy, the creative catalog, audit search and verification reports.
A TypeScript SDK with React and Vercel AI SDK entries, and a Python SDK. Both are thin clients over the same HTTP contract.
Evaluate once, on the user’s goal, at the start of the task. One audit record per user turn is the contract, and frequency caps count evaluations.
Not yet. The repository opens with the first release on npm, PyPI and GitHub Container Registry. Ask for early access if you want it sooner.
Apache 2.0.
One email when AdGate is on npm and PyPI. Nothing else.
Want it sooner, or want to be a design partner? Write to us instead.
Early access and design partnerships: hello@chalkcorner.com. Back to Chalk Corner.