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 · component map · HTTP surface · publishers · advertisers · settlement · campaign contract

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│
      └──────────────┘

HTTP surface

Root serves two renderings of the same facts: machines get JSON, browsers (Accept: text/html) get the status page.

RouteWhat it does
GET /Service status — name, version, chain connection, agency wallet. JSON for machines, the status page for browsers.
GET /healthLiveness: 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.jsThe slim publisher script. Embed from here or via your own _adnet.<domain> CNAME for first-party serving.
GET /client/adsWhich 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/statusThe matcher's signed transparency: cadence, last session, counts.
GET /exchange/api/campaignsThe Exchange's public mirror of on-chain campaigns. A cache — the contract wins every disagreement.
GET /.well-known/aiMachine-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.

RouteWhat it does
POST /publish/campaignDeploy 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}/budgetFund the campaign (adds to on-chain budget).
POST /publish/campaign/{id}/promotionAdd a creative (title, subtitle, creative, link).
POST /publish/campaign/{id}/pause · /unpauseStop / resume serving. Pausing does not block settlement of engagement already delivered.
POST /publish/campaign/{id}/settleFlush and settle accrued engagement (see settlement).
GET /publish/campaign/{id}Campaign detail, reconciled to the contract.
GET /publish/campaign/{id}/reportEngagement report: views, clicks, per-publisher breakdown.
GET /publish/campaign/{id}/eventsThe raw accrued events behind the report.
GET /publish/adsList 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.

  1. 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.
  2. 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.
  3. 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 calls settleBatch — crediting that named publisher's balance and advancing spent, 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
openAny publisher attributable to an address.
confirmedDomainOnly publishers whose Scan standing is domain-bound — a verified DNS/on-chain binding, not merely self-claimed.
whitelistedOnly 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

RoleFieldSet atAuthority
Advertiseradvertiser {name, wallet}constructorOwns and funds. Shares administration; destination of a budget refund.
Agencyagencyconstructor (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

FieldTypeMeaning
advertiser / agencystruct / addressGoverning roles.
namestringCampaign name.
budget / spentuint256 (wei)Total budget and cumulative settled spend.
impressionRate / clickRateuint256 (wei)Per-view / per-click payout rates.
pacinguint256Budget pacing window, in minutes.
targetAudiencestringDeclared audience.
settlementRulestringDeclared payment rule (open / confirmedDomain / whitelisted), enforced off-chain by the agency.
activeboolServing status.
promotions[]Promotion[]Creatives (creative, title, subtitle, link, active).
publisherBalancesmapping(address ⇒ uint256)Each publisher's accrued, withdrawable balance.
processedBatchesmapping(bytes32 ⇒ bool)Settled batch ids — enforces idempotency.

Settlement & payout

MethodAccessBehavior
settleBatch(publisher, impressions, clicks, batchId, evidenceURI, publisherSigned)advertiser or agencyCredits 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 payeeTransfers the caller's balance to the caller and zeroes it. Emits Withdrawn.

Budget & lifecycle

MethodAccessEffect
addBudget() / receive()payable, anyAdds sent value to budget. Emits BudgetAdded.
withdrawBudget()advertiser or agencyRefunds 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 agencyStop / resume serving. Pausing does not block settlement of delivered engagement.
addPromotion(…) / togglePromotion(index)advertiser or agencyManage creatives.
remaining() / getPromotion(index) / getPromotionCount()publicRead 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.