Adnet — architecture & reference
How the network fits together and how to use every route it serves. For the reasoning behind the design, see about.
Overview
All of Adnet is served from adnet.geistm.com — the Factory. A publisher
needs no host, no plugin stack, and no keys; only a script. Every public route keys off the
campaign contract address, which is the universal locator: links are baked into the
campaign rather than living in a side-table, and any surface — a web page, an email, a
server-rendered view, an AI agent — can embed an ad by fetching /render and clicking
through /link.
The contract on Polygon is the source of truth. Serving state, spend against budget, and each publisher's accrued balance live on-chain and are read back from the chain; anything the Factory caches is reconciled to the contract, with the contract winning every disagreement.
Component map
Polygon — source of truth
┌───────────────────────────────────────────────┐
│ CampaignContract Creative host │
│ budget · rates · rule (Storj-backed, │
│ promotions · balances public URLs) │
└──────▲─────────────▲──────────────▲───────────┘
deploy · fund │ settleBatch │ │ publish once
│ │ │
┌──────┴──────────────┴──────────────┴──────────┐
│ adnet.geistm.com — the Factory │
│ /publish /client /render /link │
│ /exchange (the matcher) │
└──▲─────────▲──────────────────────────────────┘
│ │ resolve publisher facts ──▶ Epistery Scan
views · │ │ signed writes to chain ──▶ Epistery Relay
clicks │ │ (rootz)
┌──────┴───────┐ │
│ Publisher │◀┘
│ site │
│ client.js │
│ _adnet CNAME│
└──────────────┘
- Adnet Factory (GeistM) — this application. Deploys campaigns, serves and routes ads, and settles as the agency.
- Adnet Exchange (GeistM) — the matcher, mounted at
/exchange. Pairs campaigns to publishers over declared data and Scan origin facts. Discovery, never required plumbing — the same Scan queries are open to any competing matcher. - Epistery Scan (Rootz, epistery.com) — the neutral index of declared origins: facts, not verdicts.
- Epistery Relay (Rootz) — a blind signed-object conduit: identifies the chain, applies fee policy, pays gas against credits, verifies signatures and on-chain authorization — and decides nothing.
- Creative host — creatives are published once to a shared files host and served by public URL, so any publisher can render them regardless of where the campaign was authored.
HTTP surface
Root serves two renderings of the same facts: machines get JSON, browsers
(Accept: text/html) get the status page.
| Route | What it does |
|---|---|
GET / | Service status — name, version, chain connection, agency wallet. JSON for machines, the status page for browsers. |
GET /health | Liveness: blockchain enabled, wallet address. |
GET /render/{format}/{campaign} | Server-rendered ad fragment (formats: banner, square, text, card, qr). Records the served view and mints an impression token. Omit the campaign and the Exchange chooses one for your site. |
GET /link/{campaign} | Click-through. Resolves the campaign's links at click time (weighted, audience-aware, macro expansion), records the click, 302s to the advertiser. The contract address is the locator; nothing else is needed. |
GET /client/client.js | The slim publisher script. Embed from here or via your own _adnet.<domain> CNAME for first-party serving. |
GET /client/ads | Which campaigns are live to render — contract address and name per candidate, reconciled against the chain. |
/publish/… | The advertiser/agency surface: build, deploy, fund, and operate campaigns (see advertisers). /api is a deprecated alias. |
GET /exchange/api/match/{domain} | Signed, self-explaining match: which campaigns fit a publisher, and why — gates passed, facts consulted, session that produced it. |
GET /exchange/status | The matcher's signed transparency: cadence, last session, counts. |
GET /exchange/api/campaigns | The Exchange's public mirror of on-chain campaigns. A cache — the contract wins every disagreement. |
GET /.well-known/ai | Machine-readable declaration of this service. |
Publishers — joining and getting paid
1. Embed the script
Mark ad slots with a class and load the client. The script finds the slots, asks
/client/ads which campaigns are live, fetches a server-rendered fragment per slot,
and injects it. There is no client-side rendering, recording, settlement, or identity code.
<div class="adnet-entry-banner"></div>
<script src="https://adnet.geistm.com/client/client.js" async></script>
Slot classes: adnet-entry / adnet-entry-banner (banner),
adnet-entry-square (square), adnet-entry-card (card).
2. Optional: serve first-party
Point a _adnet.<yourdomain> CNAME at the Factory and load the script from
it — /render and /link then run first-party. The CNAME is a real
delegation: creating that record authorizes Adnet to attribute engagement to your domain. No
private key is ever handed over.
3. Declare yourself
Serve a /.well-known/ai manifest. Epistery
Scan indexes the declaration and confirms it is actually served from the domain it claims;
the facts it records (signature standing, DNS/on-chain binding, observed age) determine which
settlement rules you qualify under — confirmedDomain campaigns pay only
domain-bound publishers.
4. Withdraw
Settlement credits your balance on each campaign contract. Call withdraw() on the
contract whenever you choose — payment is a direct transfer with an on-chain receipt. No minimum
threshold, no payout schedule, no one in between.
Advertisers — building a campaign
The /publish surface deploys and operates campaigns. Write endpoints require a
signed request from the advertiser or agency wallet.
| Route | What it does |
|---|---|
POST /publish/campaign | Deploy a CampaignContract: name, budget, per-view and per-click rates, pacing, declared audience, settlement rule, links, promotions. Budgets are stated in USD and converted to native coin at deploy time. |
POST /publish/campaign/{id}/budget | Fund the campaign (adds to on-chain budget). |
POST /publish/campaign/{id}/promotion | Add a creative (title, subtitle, creative, link). |
POST /publish/campaign/{id}/pause · /unpause | Stop / resume serving. Pausing does not block settlement of engagement already delivered. |
POST /publish/campaign/{id}/settle | Flush and settle accrued engagement (see settlement). |
GET /publish/campaign/{id} | Campaign detail, reconciled to the contract. |
GET /publish/campaign/{id}/report | Engagement report: views, clicks, per-publisher breakdown. |
GET /publish/campaign/{id}/events | The raw accrued events behind the report. |
GET /publish/ads | List campaigns. |
Settlement — accrue, flush, settle
Settlement is split so the chain does only what a chain should, and the agency does everything a chain should not.
- Accrue. Every view (
/render) and click (/link) is recorded as an event anchored to the campaign's contract address, enriched with the context at the request: publisher domain and its Scan facts, device and browser, referrer, the impression token tying a click back to its view, and a best-effort continuity id. Events accumulate off-chain as pending evidence. - Flush. At a threshold — or on demand, or on a schedule — the agency sweeps a campaign's accrued events onto its contract. Obvious spam (malformed events, replayed impression tokens) is dropped at the doorstep and never counted. The flush is signed and transacted by the agency; a batch may additionally carry a publisher's or viewer's signature as evidence riding inside it.
- Settle. The campaign declares its payment rule on the contract; the agency enforces
it off-chain using Scan's proof points. For each confirmed publisher the agency computes the
payout (
views·impressionRate + clicks·clickRate), builds an evidence bundle whose content hash is the batch id, and callssettleBatch— crediting that named publisher's balance and advancingspent, capped at the remaining budget. The batch id makes the write idempotent: a batch that already landed can never be double-paid. Events that fail the rule are marked denied — retained as evidence and reported, never silently deleted.
Rule (settlementRule) | Who gets paid |
|---|---|
open | Any publisher attributable to an address. |
confirmedDomain | Only publishers whose Scan standing is domain-bound — a verified DNS/on-chain binding, not merely self-claimed. |
whitelisted | Only publishers the advertiser named. |
Signatures are additive evidence, not gates. Every settled record carries the agency's baseline signature. A publisher's own signature — or a viewer's — is an upgrade: a higher assurance tier, not a validity requirement. The network emits verifiable evidence; each party decides how much of it a given payment requires. Trust is never mandated. It is measured.
Campaign contract — CampaignContract
One deployed contract is one campaign: identity, budget accounting, promotions, declared payment rule, publisher balances. Every read method is public — any advertiser, auditor, or regulator can call it without permission.
Roles
| Role | Field | Set at | Authority |
|---|---|---|---|
| Advertiser | advertiser {name, wallet} | constructor | Owns and funds. Shares administration; destination of a budget refund. |
| Agency | agency | constructor (the deploying Factory) | Operates and settles. Shares administration. |
The modifier onlyAdvertiserOrAgency guards the shared-admin methods and is
satisfied by the advertiser wallet or the agency address — nothing else. There is no super-admin.
A publisher is not a role: it is any address that has been credited a balance and may
withdraw() it.
State
| Field | Type | Meaning |
|---|---|---|
advertiser / agency | struct / address | Governing roles. |
name | string | Campaign name. |
budget / spent | uint256 (wei) | Total budget and cumulative settled spend. |
impressionRate / clickRate | uint256 (wei) | Per-view / per-click payout rates. |
pacing | uint256 | Budget pacing window, in minutes. |
targetAudience | string | Declared audience. |
settlementRule | string | Declared payment rule (open / confirmedDomain / whitelisted), enforced off-chain by the agency. |
active | bool | Serving status. |
promotions[] | Promotion[] | Creatives (creative, title, subtitle, link, active). |
publisherBalances | mapping(address ⇒ uint256) | Each publisher's accrued, withdrawable balance. |
processedBatches | mapping(bytes32 ⇒ bool) | Settled batch ids — enforces idempotency. |
Settlement & payout
| Method | Access | Behavior |
|---|---|---|
settleBatch(publisher, impressions, clicks, batchId, evidenceURI, publisherSigned) | advertiser or agency | Credits the named publisher with impressions·impressionRate + clicks·clickRate, advancing spent. Reverts on zero publisher, replayed batchId, or payout exceeding budget. batchId is the content hash of the off-chain evidence bundle; publisherSigned records the assurance tier. Not gated on active — a paused campaign can still settle engagement delivered while live. Emits BatchSettled. |
withdraw() | any payee | Transfers the caller's balance to the caller and zeroes it. Emits Withdrawn. |
Budget & lifecycle
| Method | Access | Effect |
|---|---|---|
addBudget() / receive() | payable, any | Adds sent value to budget. Emits BudgetAdded. |
withdrawBudget() | advertiser or agency | Refunds the unspent budget (budget − spent) to the advertiser wallet and retires the campaign, leaving each publisher's earned share intact. Emits BudgetWithdrawn. |
pause() / unpause() | advertiser or agency | Stop / resume serving. Pausing does not block settlement of delivered engagement. |
addPromotion(…) / togglePromotion(index) | advertiser or agency | Manage creatives. |
remaining() / getPromotion(index) / getPromotionCount() | public | Read helpers. |
Events (indexer surface)
BatchSettled, Withdrawn, PromotionAdded,
PromotionUpdated, CampaignPaused, CampaignUnpaused,
BudgetAdded, BudgetWithdrawn.
BatchSettled(publisher, submitter, impressions, clicks, payout, batchId, evidenceURI,
publisherSigned) is the canonical settlement record: the paid publisher, the submitter,
the counts and payout, and the hash tying the on-chain summary to the agency-signed evidence
bundle.