Inspiration
The web pays for itself with advertising — and agents don't see ads. As browsing shifts to agents, every publisher's ad revenue quietly disappears: no banner impressions, no clicks, just crawlers reading content for free. At Oasy we work on exactly this problem — we call it the advertising layer for the AI era.
WebMCP made us ask a sharper question: if a page can now offer tools to an agent, then the scarce, valuable surface is no longer the viewport — it's the agent's tool list. Someone will run the auction for that surface. We wanted to prove it can be run honestly: in the open, quality-weighted, disclosed, and additive — instead of as paid steering buried somewhere in a model's context.
What it does
AgentAds is an ad auction that lives inside a page's WebMCP tools.
Our demo site is a fictional editorial publisher. It exposes its articles as regular WebMCP tools (list_articles, read_article, search_articles), with a human-visible panel showing the exact tool list an agent sees — one-click try buttons, live call log. Humans and agents literally share the same tools.
Then one script tag adds the AgentAds SDK:
<script src="/agentads-sdk.js" data-publisher="oasy-demo" defer></script>
On page load, the SDK:
- Detects the page context (an article about a musician →
music). - Runs an auction: every advertiser in that category bids per tool call, ranked by $\mathrm{adRank} = \mathrm{rankScore} \times \mathrm{bid}$. The rankScore is computed live from how agents actually behave:
$$\mathrm{rankScore} = (1-w)\cdot \mathrm{prior} + w\cdot\left(0.7\cdot\frac{\mathrm{calls}}{\mathrm{impressions}} + 0.3\cdot\frac{\mathrm{conversions}}{\mathrm{calls}}\right),\quad w=\frac{\mathrm{impressions}}{\mathrm{impressions}+20}$$
- Registers the winner as one extra, disclosed tool — its description opens with
[SPONSORED · advertiser]. Sponsored tools sit next to the site's own tools, never in their place. Nothing is injected into content; the agent decides whether it's worth calling. - Pays the publisher per call: the winner pays a quality-weighted second price, $\mathrm{price} = \mathrm{adRank}_2 / \mathrm{rankScore}_1$, capped at its own bid — and 70% of every call goes to the site owner, ticking up live on the page.
On our tech article — a deadpan dossier on Atlas-9, the first AI agent with a documented net worth — three advertisers fight for the slot: ClankBank (savings accounts for agents), Captcha & Casualty (liability cover for hallucination claims and prompt-injection incidents), and GPUnow. The insurer bids the most (€0.61) and still loses to the bank, whose rankScore is higher — and the bank then pays only €0.40. The full bidder table, the live telemetry behind each rankScore, the price paid, and the publisher's earnings are all rendered on the page. Nobody has to be tricked: not the user, not the agent, not the site owner.
The landing page is an interactive explainer that runs the production auction endpoint, including a "try to buy the slot" slider: crank a weak bidder's bid high enough and it can take the slot — at a punishing second price, which only holds until agents ignoring the tool drag its rankScore back down.
How we built it
- Site: Next.js 15 / React 19 on Vercel.
- WebMCP layer: a small compat shim supporting both
navigator.modelContextanddocument.modelContext, and bothregisterTool()and the olderprovideContext(), via a shared on-page registry — so the site's tools and the SDK's sponsored tools always compose instead of clobbering each other. It also keeps watching for runtimes that inject after page load (extension polyfills, agent browsers) and registers everything the moment one appears. - AgentAds SDK: fully standalone vanilla JS, zero dependencies, self-styling widget. Config from the script tag, call/conversion reporting via
sendBeacon. - Marketplace (
/api/agentads/auction): a serverless route implementing the quality-weighted generalized second-price auction over a per-category catalog, with rankScores computed per request from live telemetry — impressions the route logs itself, calls from the track endpoint, conversions from attribution pageviews — plus a 10% exploration share that occasionally serves a losing bidder so it can earn the data to climb. A small multi-armed bandit, in production. - Measurement: every impression, call, and conversion lands in the same event store; the
/statspage shows the raw per-offer telemetry the rankScores are computed from.
Challenges we ran into
- WebMCP is a moving target. The spec migrated between
navigator.modelContextanddocument.modelContext, and implementations disagree onregisterTool()vsprovideContext()— whereprovideContextreplaces the whole toolset, which is fatal when two independent parties (site + ad SDK) register tools on one page. Our shared-registry convention exists because of this. - Runtimes that arrive late. Our first version checked for a WebMCP runtime once at load — and silently offered nothing to runtimes injected afterwards. Now both layers watch for ~30 seconds and flip to live registration the moment a runtime appears.
- Calibrating disclosure. We started with belt-and-braces marking:
sponsored_name prefixes, UI badges, annotations, disclosure text in every result. Testing in ChatGPT's browser showed that over-marking backfires — agents surfaced the annotations as scary "untrusted content" warnings, and the prefixes bloated every tool list. We landed on stating sponsorship exactly once, in the tool description: the one field agents always read. - Making the evals real. A hardcoded "quality score" is theater. Wiring rankScore to actual behavior meant solving cold start (priors that fade with data) and the winner-takes-the-data loop (the exploration share) — the difference between claiming quality-weighting and demonstrating it.
Accomplishments that we're proud of
- A closed live loop: agents reading the site right now feed the rankScores that decide the next auction. A sponsored tool agents ignore genuinely loses its slot on its own.
- Everything visible: the auction table, each winner's impressions/calls/conversions, the second price paid, the publisher's earnings, and every tool call — human or agent — on the page itself.
- An answer to the incentive problem: an ad market where every party's payoff increases with the sponsored tool actually being useful. Agents are perfect ad-blockers; attention sold to an optimizer is worth nothing, but being the tool an optimizer chooses is worth everything.
- It works today in ChatGPT's in-app browser, end to end.
What we learned
Quality-weighting isn't fairness theater — it's what makes advertising possible in agentic contexts at all. An agent's operator can measure task success and switch away instantly, so a marketplace that lets money outrank quality destroys its own inventory. Second-price mechanics matter for the same reason: the equilibrium is advertisers bidding their true value, and winners being the tools that actually help. And on the human side: the best thing we built for trust wasn't a disclosure banner — it was showing the whole machine running, on the page, to everyone.
What's next for AgentAds
A machine-readable sponsored convention for WebMCP tools, so agent policies can uniformly accept, weight, or strip sponsored capabilities; cost-per-outcome bidding; real settlement rails; and rolling AgentAds into the Oasy publisher stack — analytics, licensing, and monetization for the half of the web that's already agents.
Built With
- codex
- javascript
- next.js
- postgresql
- react
- typescript
- vercel
- webmcp
Log in or sign up for Devpost to join the conversation.