Auctionly

EXCHANGES + PUBLISHERS + WALLET

Your own ad network, self-hosted.

Auctionly is the bidder, campaign manager, publisher portal and billing for running your own ad network. Buy from ad exchanges over OpenRTB 2.5 with a bid out in under 100 ms, or from publishers you onboard yourself with a verified site and one ad tag. Seven ad formats, and every record on a server you control.

View docs

You deploy it. You own the database. Nothing phones home.

requestexample
POST /bid/xyz-ssp
Content-Type: application/json

{ "id": "8f2c…", "tmax": 120,
  "imp": [{ "id": "1", "banner": { "w": 320, "h": 250 } }],
  "device": { "os": "android", "geo": { "country": "USA" } } }
response200 · served from Redis
{ "seatbid": [{ "bid": [{
    "impid": "1", "price": 2.40,
    "nurl": "…/win?sig=hmac" }] }] }

no match → 204 + X-Nobid-Reasons: targeting:country=3
publisher siteno ad server needed
<!-- one tag, pasted where the ad goes -->
<script async src="…/t/z-4f2a91c07b.js"></script>

verified site → ad served → impression counted → paid
LanguagePHP 8.3
ProtocolOpenRTB 2.5
RuntimeDocker-native
DataPostgreSQL, Redis, Elasticsearch
PaymentsStripe and bank transfer
Static analysisPHPStan level 8
How it works

From funded wallet to billed impression.

Advertisers, and two ways to bring supply: exchanges over OpenRTB, or publishers you onboard directly. One event stream either way. Each step writes to rtb:events, so billing and reporting read the same facts the bidder produced.

  1. STEP 1

    Advertisers fund and launch

    Sign up, add funds by card or bank transfer, create a campaign in any of seven formats. It reaches the bidder about three seconds after you save it.

  2. STEP 2

    Supply arrives from exchanges or publishers

    An exchange registers once and posts OpenRTB 2.5 to its own /bid/{exchange} endpoint. A publisher verifies a site, creates a zone and pastes one tag or sends visitors to a URL.

  3. STEP 3

    A campaign is picked, billing happens once

    Exchange traffic runs an auction inside the exchange's tmax. Publisher traffic goes to matching campaigns in turn at their fixed prices. The aggregator debits the wallet and credits the publisher one time per impression.

Life of one impressionEvery hop is HMAC-signed
  1. Bid/bid/{exchange}Auction runs against in-memory campaigns.
  2. Win/winExchange reports the clearing price. Wallet is debited, once.
  3. Pixel/pixelAd renders and the impression is confirmed.
  4. Click/clickRedirect to the advertiser's page, click recorded.
  5. Conversion/conversionPostback matched to the click inside its window.
Billing event. A win delivered twice is charged once. rtb:events Aggregator writes stats and spend. Indexer writes each event to Elasticsearch.
Life of one publisher viewNo auction, no ad server
  1. Tag/t/{zone}.jsOne script on the publisher's page adds a sized, sandboxed frame.
  2. Check/a/{zone}A real browser, on the zone's own site, within the zone's limits.
  3. PickrotationMatching campaigns take turns, each at its own fixed price.
  4. CountimpressionServed is counted. Advertiser debited, publisher credited, once.
  5. Land/clickRedirect to the advertiser with a click id. A conversion postback matches it.
Redirect traffic (pop, domain, deeplink) counts the view and the click in one visit. /r/{zone} The same event stream feeds billing, publisher earnings and reports.
Ad formats

Seven formats, one campaign form.

Pick a format and the form asks for exactly the creative that format needs. The web form and the REST API share one rule set, so neither can save a campaign the other would refuse.

DisplaydisplayBanner image or HTML in the sizes the exchange requests.
Video (VAST)videoA video file or VAST tag, matched to the player size.
NativenativeTitle, description, icon and image that the publisher styles.
PushpushNotification title, body, icon and a destination URL.
PoppopA destination URL. No creative to host.
Domain redirectdomainA destination URL for redirect traffic.
DeeplinkdeeplinkAn app deeplink or universal link as the destination.

Through exchanges all seven formats run. Publishers you onboard directly sell display through an ad tag, and pop, domain redirect and deeplink through a traffic URL. Video, native and push need an ad server, so they stay on OpenRTB.

Display, video and native are read from the OpenRTB imp objects. Push, pop, domain and deeplink are not, so the exchange either sends imp.ext.adformat or is registered with a traffic type.

Billing

Card or bank transfer. Credited when the money is real.

Advertisers fund a prepaid wallet. Spend is debited as impressions are won, and a campaign pauses when its funds run out. You choose which funding methods to switch on.

Card via Stripe

stripe checkout
  1. Advertiser pays on Stripe's hosted checkout.
  2. Stripe confirms with a signed webhook.
  3. Wallet is credited. Never because a browser said so.
  • A worker asks Stripe about anything still pending every minute, in case a webhook was lost.
  • A refund in Stripe debits the wallet.
  • No Stripe SDK. The app calls Stripe's HTTP API directly, so there is less code to audit.

Bank transfer

admin approved
  1. Advertiser requests a top-up and sees your bank details.
  2. You check your account and enter the amount received and the bank reference.
  3. Wallet is credited once you approve.
  • Requests nobody approves expire on their own.
  • Leave your bank details empty and the option disappears. Run card-only or transfer-only.
  • Implausible amounts are refused, and every confirmation or rejection lands in the audit log.
Minimum top-up
$10
Maximum top-up
$10,000
First top-up cap
$500
Billing and tax details
advertisers and publishers

Limits are defaults you can change in the environment file. The first-top-up cap applies until an advertiser's first payment has settled, which limits the damage from a stolen card. The wallet balance shows in the top bar, with a warning when it will not cover the recent daily spend. Every credited top-up has a printable receipt with your company details.

Architecture

Built for scale, not demos.

The path that answers an exchange is kept small and separate from everything that can be slow.

Hot path · answers the exchange

exchangepublisher sitebidderredis

The bidder reads campaigns from memory and Redis, for exchange bids and publisher views alike. It never opens a database connection, so a slow Postgres cannot slow a bid.

Cold path · money, reports, admin

campaign-managerpostgresaggregatorindexerelasticsearchreportingoptimizer

Campaigns, wallets and users live in PostgreSQL. Events flow through a Redis stream to the aggregator and the indexer, at their own pace.

Redis only on the bid path

Campaigns are held in memory. The bidder has no database driver in its request loop.

Exactly-once billing

An impression delivered twice is charged once. Events are keyed by stream id, so a redelivery overwrites itself.

Under 100 ms target

A per-request deadline is derived from the exchange's tmax. make latency measures it with wrk on your hardware.

Fuzz-tested input

Seeded random malformed bid requests must never crash the parser or the auction. make fuzz

Chaos-tested outages

Redis, Postgres, Elasticsearch, mail and a killed worker are injected on purpose. make chaos

Fails in the exchange's favour

If Redis is down, win notices answer 503 so the exchange retries instead of losing the sale.

Images stay off the app server

Banner pictures are served from a media domain by Caddy straight from disk, with immutable caching. Point a CDN at it when traffic grows.

Publishers do not slow the bidder

Zones live in their own Redis hash, are fetched on demand and cached for seconds. A new zone never makes every bidder reload the campaigns.

Money is bounded per zone

Daily ceilings per address and per zone cap what any one zone can cost advertisers, whatever a script does. TAG_VIEWS_PER_ZONE_PER_DAY

Every click is kept for two jobs

Counts and spend go to PostgreSQL for billing. The raw click goes to Elasticsearch for search and reports, kept 90 days by default.

Inside the box

Everything an ad network needs, in one repository.

Seven services, one shared kernel, one Docker Compose file. Read it, change it, ship it.

campaign-manager

Web app and REST API

One campaign form with ad-format cards, budgets, bid hierarchy, delivery limits, creatives, targeting and conversion setup. The API enforces the same rules.

reporting

Reports on every dimension

Totals, time series and breakdowns by campaign, day, hour, exchange, bid level, creative, country, OS, device, site, publisher, ad format and zone. CSV export and event-log search.

optimizer

Bids that follow results

Give a campaign a CPA or CTR goal and a bid multiplier moves toward what converts, without anyone touching the bid.

placements

Whitelist, blacklist, bid per zone

Block or allow placements, share large lists between campaigns, and set a bid for one zone straight from the report.

tracking

Landing-URL tags and postbacks

Insert tags like {click_id}, {zone} and {country} into destination URLs. A postback builder writes the conversion URL for your tracker.

limits

Budgets, pacing, caps

Daily and lifetime budgets, pacing, frequency caps, schedules, impression and click limits. A campaign that hits one pauses and records why.

metrics

Operable from day one

The bidder serves /health and Prometheus /metrics: requests by exchange and outcome, rejections by reason, a latency histogram.

mail

Queued email with retries

Sign-up, password reset and payment email goes through a queue with retries after 1, 2, 4 and 8 minutes. Bodies are erased once sent.

demo

Realistic data in one command

make fake-data creates advertisers, wallets and campaigns. make traffic sends 300 requests through bid, win, pixel, click and conversion.

review

Approve before it bids

An advertiser's campaign waits in review until you approve it or send it back with a note. Trusted advertisers can skip it.

receipts

Receipts, Terms, Privacy

Every credited top-up has a printable receipt with your company details. Sign-up records which version of the Terms was accepted.

publishers

Portal for sites, zones and payouts

Publishers sign up, add a site, create zones and copy a tag. Earnings, payout progress and billing details are in their own area.

verification

Ownership proven before traffic

A meta tag, a file or a DNS record proves a site is the publisher's. You approve it, and only then can it have zones.

zones

Thousands of zones, one snapshot

Zones are looked up on demand and kept out of the campaign snapshot, so adding publishers never slows a campaign reload.

media

Image upload and a CDN-ready domain

Upload creatives in the form or with POST /api/v1/media. Serve them from their own domain, cached for a year, with a CDN in front when you need one.

For advertisers

Self-serve buying, with the controls media buyers ask for.

Supply
Buy from exchanges, from your own publishers, or both, per campaign. Publisher-only campaigns pay a fixed price per view, click or conversion.
Wallet
Fund by card (credited only after Stripe confirms) or by bank transfer approved by an admin.
Targeting
Geo (country to ZIP), device, OS, carrier, domain, app, zone, category, keyword, audience.
Conversions
Postback builder, a window per campaign, and conversions that keep the click's format, zone and country.
Attribution
Six attribution models.
Limits
Daily and lifetime budgets, pacing, frequency caps, schedules.
REST API
Token-based, using the same validation as the web form.

Bid hierarchy · most specific wins

  1. Zone z-100
  2. Site, app or publisher news.example
  3. Category IAB12
  4. Exchange xyz-ssp
  5. Campaign default bid

Pay more on the zone that converts, and the default everywhere else. Ties are split at random.

Quality control

Every campaign is reviewed before it bids.

Advertisers cannot put a campaign live by themselves. You decide what runs on your network, and the advertiser always hears the outcome.

  1. SUBMITTED

    Save and submit

    The advertiser presses submit. The campaign is In review and does not bid.

  2. YOU ARE TOLD

    Email and a queue

    Administrators get an email and see it on the dashboard, in the In review tab and as a count in the navigation.

  3. DECIDED

    Approve or send back

    Approve and it bids within seconds. Or send it back to draft with a note, which the advertiser is emailed.

  4. AFTERWARDS

    Stays approved

    Pausing and resuming needs no new review. Changing a creative or its landing URL sends it back.

  • Trusted advertisers skip the queue once you have seen their work.
  • The API follows the same rule. A token that asks for active gets 202 and pending_review.
  • One switch. REQUIRE_CAMPAIGN_REVIEW=false turns it off for everyone.
  • Every decision is in the audit log, with who made it and the note.

Why it matters. Exchanges hold you responsible for what you show. A review step is the cheapest protection against a misleading ad costing you a supply partner.

For developers

Everything in the web app is in the API too.

Ten endpoints under /api/v1, JSON in and out, one personal token per script. An advertiser's token only ever sees that advertiser's campaigns, and the same validation applies as in the form.

  • GET /api/v1/me

    Who the token belongs to.

  • GET /api/v1/advertisers

    The accounts you can see, with wallet balance.

  • GET /api/v1/campaigns

    List with filters, paging and sorting.

  • POST /api/v1/campaigns

    Create. The body is the campaign form.

  • GET · PUT · DELETE /api/v1/campaigns/{id}

    Read, change or delete one campaign.

  • POST /api/v1/campaigns/{id}/status

    Activate, pause, draft or archive. Activating can answer 202 while it waits for review.

  • POST /api/v1/media

    Upload a creative image and get its address.

  • GET /api/v1/reports/campaigns

    Spend, clicks and conversions per campaign.

upload, create, go liveexample
$ curl -X POST $BASE/api/v1/media \
    -H "Authorization: Bearer $TOKEN" \
    -F image=@banner.png
{ "data": {
    "url": "https://media.example.com/media/ab/3f…c1.png",
    "width": 320, "height": 250 } }

$ curl -X POST $BASE/api/v1/campaigns/123/status \
    -H "Authorization: Bearer $TOKEN" \
    -d '{"status":"active"}'
HTTP 202
{ "data": { "id": 123, "status": "pending_review" } }
For exchanges

Connect an SSP in minutes.

An administrator registers the exchange with its token, seat, network margin and traffic type. The exchange gets one endpoint.

  • A dedicated endpoint at /bid/{exchange}, token-checked, pausable.
  • OpenRTB 2.5 request, impression, device, geo, deal, video and native objects.
  • Deal support as a targeting dimension on each campaign.
  • Traffic-type classification for pop, push and redirect supply that OpenRTB cannot describe.
  • Win-price macro and signed win, pixel and click URLs per exchange.
  • Plain no-bids. A 204 with the reason in a header, so partners can debug.
try it locally
$ curl -X POST localhost:9501/bid/demo \
    -H 'Content-Type: application/json' \
    -d @services/bidder/fixtures/sample-bid-request.json

$ make win
# bid, win notice (sent twice), pixel, click
# wallet debited once
For publishers

Onboard site owners who have no ad server.

Most publishers cannot speak OpenRTB. They sign up, add a website, prove it is theirs and paste one tag. There is no auction here: matching campaigns take turns at fixed prices, and the publisher is paid on terms you set.

  1. SIGN UP

    Submit a site

    A publisher creates an account and adds the website it sends traffic from. The site waits for review.

  2. VERIFIED

    Prove it is theirs

    A meta tag, a file at the root, or a DNS TXT record. Any one is enough, and the check refuses private addresses.

  3. APPROVED

    You review the site

    Approve or reject with a reason, and the publisher is emailed. Suspend a site and its zones stop at once.

  4. LIVE

    Create a zone, paste the tag

    Only an approved site can have zones. A Get tag page gives copy-and-paste code and says what happens next.

  • Two ways to send traffic. An ad tag shows a banner on the page. A traffic URL sends visitors to the advertiser in one redirect.
  • Fixed prices, no auction. Campaigns that match the visitor rotate. Advertisers pay per view, per click or per conversion.
  • Pay on your terms. A share of the advertiser's spend, or a fixed amount per view, click or conversion, per publisher or per zone.
  • Payouts with progress. Requested, processing, paid, with the transfer reference and an email at each step. Earnings are held for a few days first.
  • Billing and tax on file. Legal name, address and tax id are required before a payout, and kept with each one.
  • Campaigns choose their supply. Exchanges, publishers or both, per campaign. A publisher-only campaign can pay per conversion.
what a publisher pastesexample
<!-- display zone: a banner on the page -->
<script async
  src="https://bid.example.com/t/z-4f2a91c07b.js">
</script>

<!-- pop, domain, deeplink: send the visitor -->
<a target="_blank"
  href="https://bid.example.com/r/z-9c13">Open</a>

verified site → zone → tag → earnings on the dashboard
portal

Sites, zones, tags

A portal of their own: sites and their review status, zones, copy-and-paste tags and a dashboard of impressions, clicks, conversions and earnings.

earnings

Exact, once

Earnings are credited together with the advertiser's charge, one time per impression, with the terms that applied kept on each row.

payouts

Requested to paid

A payout moves through requested, processing and paid, with a reference. The money leaves the balance when it is asked for, and comes back if it is rejected.

admin

A queue for you

Sites waiting for review, payouts to send, quality flags per zone, and a margin view of what advertisers paid against what publishers earned.

Traffic quality

When a publisher is paid for what it counts, every count is checked.

Exchanges are contracted companies. Publishers are anyone who signs up, so the platform assumes any number a publisher produces could be false and bounds what it can cost.

Verified sites only

A zone serves traffic only while its site is approved. Suspending a site stops its zones at once.

The right site

An ad is served only to a page on the zone's own site. Copying the tag onto another site gets nothing.

Real visitors

Bots, crawlers and scripts are not sold. Country and device come from your edge network, not from the publisher.

Daily ceilings

Per address and per zone, per day, whatever browser string is used. Raise a limit for a large site after a review.

Signed, fixed prices

Tracked URLs carry an HMAC. A forged price, campaign or level is refused, and a click or win counts once.

OpenRTB off by default

A publisher's own bid requests prove nothing, so the endpoint is opt-in per zone for partners you trust.

Payout hold

Earnings are held for a few days so bad traffic can be taken back with an adjustment before the money leaves.

Quality flags

Zones with a very high click rate, no conversions, or a payout above what advertisers pay are flagged for you.

Honest limits. A redirect has no page to inspect, and a script can fake a referrer. The limits, the hold and the flags are what bound the cost, not a promise that no fake traffic exists.

Security

Money and tracking you can defend.

Two-factor, enforced

Admins must enable TOTP. Recovery codes and a 48-hour cancellable lost-phone flow.

Signed tracking URLs

Win, pixel and click URLs carry an HMAC, so they cannot be forged.

Audit log

Sign-ins, account changes and admin actions are recorded.

Stripe-confirmed only

Cards are credited after Stripe confirms. A worker reconciles lost webhooks.

Automated backups

make backup dumps the database and creatives, keeping as many as you set.

Confirmed sign-ups

Switch it on and a new account cannot sign in until it opens a one-time link. Only a button confirms, so mail scanners cannot use the link up.

Terms on record

Sign-up requires accepting the Terms and Privacy Policy, and stores which version and when.

Abuse limits

Failed sign-ins lock out for 15 minutes. Sign-up, password reset, confirmation email and uploads are rate-limited per address.

Deployment

Runs anywhere Docker does.

Every service is its own container. Start on a laptop, then move the same images to a server.

One-server quick start development

shell
$ cp .env.example .env
$ make install
$ make up
$ make migrate
$ make seed
→ campaign manager on :9502, bidder on :9501

Seeds an admin, demo advertisers, campaigns and exchanges. make traffic then sends 300 requests through bid, win, pixel, click and conversion.

Production Caddy · HTTPS

docker-compose.prod.yml
internet → caddy :80 :443
          ├─ campaign-manager
          └─ bidder
private network only:
  postgres · redis · elasticsearch
  reporting · aggregator · indexer

Caddy is the only published service. It issues certificates, speaks HTTP/2 and limits request sizes. The written guide takes a fresh Linux server to HTTPS in about 45 minutes, plus DNS.

Go-live check before the first customer

./scripts/prod-check.shexample
  ok   DB_PASSWORD looks strong
  ok   administrators must use two-factor
  ok   Stripe is in LIVE mode
  FAIL LEGAL_REVIEWED is not true
  FAIL COMPANY_ADDRESS is empty

NOT READY: 2 blocking problem(s)

It refuses to pass while a secret is weak, debug is on, email goes nowhere, Stripe is in test mode or your company details are missing. The owner's guide walks from an empty server to your first advertiser.

Images and a CDN optional

.env
MEDIA_DOMAIN=media.example.com
MEDIA_BASE_URL=https://media.example.com
# or the address of a CDN that pulls from it

uploads  →  ads.example.com   →  disk
viewers  →  media.example.com →  disk

Uploads always go to the app and are saved on your server. The media domain only reads and serves them, so ad images never load from the web app.

Fully self-hosted. There is no Auctionly cloud. Campaigns, wallets, events and creatives stay on your infrastructure, and your data never leaves it.

Pricing

Buy the source. Run it as your own.

You receive the code and deploy it yourself. Choose how much help you want with it: none, ongoing support, or a white-label setup we plan with you.

Tier A

Source licence

$999 one-time

The code, sold once. No consultation and no support: you read it, you run it.

  • Full source for all services
  • One payment, no renewal
  • No consultation, no support
Tier B

Subscription

$1,499

Everything in Tier A, with a team behind it when something needs answering.

  • Everything in Tier A
  • Continuous updates
  • Support channel
Tier C

White-label

Your brand on the whole platform, with our support and consultation. The terms are agreed in a conversation, not listed.

  • Everything in Tier A and B
  • Support and consultation included
  • Price defined together

Prices are in US dollars.

Questions

Answers for people who have evaluated RTB platforms and ad networks before.

Is Auctionly a hosted service?

No. Auctionly is self-hosted software. You deploy it with Docker on your own server, and your campaigns, wallets and events stay on your infrastructure.

Which OpenRTB version does it support?

OpenRTB 2.5. Each exchange gets its own /bid/{exchange} endpoint and receives a bid, win notices and tracked clicks.

How fast does the bidder respond?

The target is under 100 ms, typically a few milliseconds. The bidder never touches the database on the bid path, and it applies a deadline derived from the exchange's tmax.

Can publishers send traffic without an ad server?

Yes. A publisher signs up, adds a website, verifies it and pastes one ad tag, or sends visitors to a traffic URL. There is no OpenRTB and no auction on that path: matching campaigns take turns at their own fixed prices.

How do you verify that a publisher owns a site?

The publisher proves it with a meta tag on the home page, a file at the root of the site, or a DNS TXT record. An administrator then approves the site, and only an approved site can have zones. Suspending or rejecting a site stops its zones at once.

How are publishers paid?

On terms you set per publisher or per zone: a share of what the advertiser spent, or a fixed amount per impression, click or conversion. Earnings are credited once, held for a few days, and paid out on request, with progress from requested to processing to paid.

How do you stop fake traffic from publishers?

Ads are only served to the zone's own approved site, bots are not sold, and limits per address and per zone per day bound what any zone can cost. Tracked URLs are signed, OpenRTB is off by default for publisher zones, and earnings are held so bad traffic can be taken back. Zones with odd click rates are flagged for review.

Which ad formats can advertisers run?

Seven: display, video (VAST), native, push, pop, domain redirect and deeplink. Through publishers you onboard directly, display runs through an ad tag and pop, domain redirect and deeplink through a traffic URL.

How do advertisers pay?

They fund a prepaid wallet by card through Stripe or by bank transfer. Card payments are credited only after Stripe confirms them. Bank transfers are credited when an administrator approves the request.

Can an impression be billed twice?

No. An impression that is delivered twice is charged once. Events are keyed by their stream id, so a redelivered event overwrites itself instead of billing again.

What happens if Elasticsearch goes down?

Reporting falls back to PostgreSQL rollups. The dimensions that only the event log knows are then reported as unavailable, and bidding is not affected because the bidder only uses Redis.

Are campaigns reviewed before they go live?

Yes, by default. An advertiser's campaign waits in review until an administrator approves it or sends it back with a note, and the advertiser is emailed either way. Administrators can mark an advertiser as trusted to skip the review, or switch the review off.

Can advertisers use an API?

Yes. The REST API has ten endpoints for campaigns, status changes, image upload and reports, with personal tokens and the same validation as the web form. An advertiser's token only sees that advertiser's campaigns.

Do banner images need a CDN?

Not at first. By default the web app serves uploaded images. When traffic grows, serve them from a separate media domain, cached for a year, and put a CDN in front of it. Uploads always go to the app; the media domain only serves files.

Own your ad stack.

Run the demo on your machine, then put it on your own server.

make stan · make fuzz · make chaos
$ make stan    # PHPStan level 8, every service
$ make fuzz    # malformed bid requests
$ make chaos   # kill redis, postgres, elastic

We reply by email.