Skip to content
Pre-alpha, open source, Apache 2.0

The open source gateway for ads inside AI chat and agents.

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.

What happens on every turn.

Five stages, one HTTP call, running in parallel with your model. The latency target is a p95 under 300 ms with a warm cache.

  1. Classify

    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.

  2. Decide

    A policy written as YAML. Blocked categories, paid tiers, regions, frequency caps, confidence floors. If anything fails, the answer is suppress.

  3. Mediate

    Fetch candidates from the app’s own sponsors and affiliate accounts, in parallel, 250 ms each. Drop competitors. Rank the rest.

  4. Disclose

    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.

  5. Record

    Write one audit record per turn, served or suppressed. Hash it, chain it to the app’s previous record, sign it with Ed25519.

Built to fail closed.

An ad that should not have shown is far worse than a missing ad. Every default leans that way.

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.

Policy as code

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.

Paid users never see ads

Unless the policy says so explicitly. Tiers, regions and locales travel with every request.

Frequency caps

Per session, per user per day, and a minimum number of turns between slots.

Labeled and separate

The label, the placement and the separation are part of the record. Inline is not an option.

A record anyone can verify.

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

  • turn_7serveSponsored, after the answer
  • turn_8suppresssensitive_category:health
  • turn_9suppresspaid_user

GET /v1/verify/aud_01J9QX

valid
true
schema
ok
record_hash
ok
chain
ok
signature
ok
creative_hash
ok
disclosure_present
ok
separation_attested
ok
Seven checks anyone can run against the record with the app’s public key. Nothing here trusts the gateway.

Bring your own demand.

AdGate mediates between sources you already have. It does not sell inventory and it takes no cut.

Direct sponsors

Your own catalog of creatives, with targeting and competitor exclusions.

Affiliate accounts

Links built with your own affiliate accounts. You keep the commission.

Network adapters

A plug-in interface for ad networks. Adapters ship once partner APIs are confirmed.

Integrate in an afternoon.

One client per process, one call per turn. The API key stays on your server; the browser only ever sees the decision.

TypeScript, with the Vercel AI SDK@adgate/sdk
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,
});
Python, any frameworkadgate
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)
Policy, one file per apppolicy.yaml
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: 90
Run it yourselfPostgres 16
docker 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 :3000

What AdGate is not.

Most of the design is refusal. These four lines are not negotiable and they are written into the contract docs that the code implements.

  • Not an ad network. AdGate owns no demand. Apps bring their own sponsors, affiliates or networks.
  • Not a way to put ads inside an answer. The slot is a separate, labeled block after the model’s reply. The SDK refuses inline.
  • Not a profiler. It stores hashes and categories, never the conversation, unless your policy says otherwise.
  • Not a hosted platform you depend on. Self hosted next to your app. The only infrastructure is Postgres.

Where it stands.

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.

  1. Now

    Hardening the deployment path and the release pipeline. Design partners welcome.

  2. 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.

  3. Later

    Network adapters against real partner APIs, cross-device frequency caps, and an advertiser self-serve portal, once real traffic says they matter.

Questions developers ask.

What happens when the classifier or a demand source is down?

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.

Can an ad ever appear inside the model’s answer?

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.

What does AdGate store?

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.

What do I need to run it?

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.

Which languages are supported?

A TypeScript SDK with React and Vercel AI SDK entries, and a Python SDK. Both are thin clients over the same HTTP contract.

How do I integrate an agent rather than a chat?

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.

Is the code public?

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.

What is the license?

Apache 2.0.

Get the first release.

One email when AdGate is on npm and PyPI. Nothing else.

Early access and design partnerships: hello@chalkcorner.com. Back to Chalk Corner.