daylight.ad Quiet · 1 day ago

All docs

Three ways to put a unit on your page, all served from plainsun.com, all returning the same ad and counting the same way. Your site key is on the site’s own page in your dashboard; every snippet below uses YOUR_SITE_KEY in its place.

Three ways in

All three carry the disclosure mark, and it is not optional on any of them.

1 · Script and placeholder

recommended

We fetch the ad and render it. Your fallback content shows if JavaScript is off or /f.js is filtered.

The stylesheet is optional. Layout and colors come from the site’s page on your dashboard, and your own CSS overrides any of it.

<script async src="https://plainsun.com/f.js" data-site="YOUR_SITE_KEY"></script>
<link rel="stylesheet" href="https://plainsun.com/f.css">
<div data-daylight>
  <a href="https://your-site.example">Your fallback content — publisher-owned</a>
</div>

2 · Raw JSON API

server-side

You fetch the ad and render it however you want. No JavaScript of ours on the page at all.

Pass a nonce and the server will not serve you the same ad twice for that page. With nothing to serve you get 200 and {"ad": null} not an error.

GET https://plainsun.com/api/v1/unit?site=YOUR_SITE_KEY

3 · Hand it to an agent

for an agent

Your coding agent does the install and picks which method to use.

It covers the site key, domain check, placement, your fallback content, and the two calls that prove the unit is counting. Read it yourself at /SKILL.md

Add Daylight ads to this site.
Read https://daylight.ad/SKILL.md and follow it.

Ads, sites and boards

The three things an account can hold, and what an ad looks like when it reaches somebody else’s page. One account can hold any of them, or all three.

Ads

shown on other sites

A product of yours, listed in the library and shown in units on other people’s sites. Paste its address into the product door: we read the page for a title, a description and a picture, show you the ad, and it goes live when you say so — there is no review queue. You do not need a site of your own.

Where it runs
On every site in the network except the product’s own domain, in whichever layout that site picked. All three are drawn below.
What you can change
Its title, description, address and category, on its own page in your dashboard. The picture comes from the product’s page, and a button there reads the page again for a new one.
One product, one ad
A domain already on the network is not listed a second time, because two ads would split one product’s numbers between them. You are pointed at the one it has.
Its public page
Every impression, every click, and every click we refused to count, with the reason. Anybody can read it, and it shows the same numbers you see.

What an ad looks like

3 layouts

The same listing in each layout a site can pick. The layout belongs to the site, not to the ad: it is set on the site’s own page in your dashboard, and every unit on that site uses it. Every layout carries the disclosure line, and none of them can leave it out.

A real listing from the library, drawn by this server in the widget’s own markup. It links straight to the product and counts nothing, because nothing on this page was served. The colours are this page’s; yours are the ones set on the site’s own page, or your own CSS.

An ad from a board

via a board

A listing that a board added is the same as any other ad. It has the product’s own words and picture, in whichever layout the site picked. The one difference is the disclosure line under it.

No board has a listing running right now, so there is nothing real to draw here. When one does, its disclosure line names the board and links there.

Sites

shows the library

A domain of yours that shows the library. Add one at the site door and you get its tags and its site key, which is the YOUR_SITE_KEY in every snippet on this page. Paste the tags where the unit goes and the slot fills from the first request. Adding a site lists nothing of yours.

The site key
Public by design: it is in the HTML of every page that runs a unit. It names the site, never you.
The domain
A unit counts only on the domain its site was registered to. Anywhere else it is still served, so a typo never leaves a page blank, but it counts nothing and the refusal is published with its reason. www. is the same site; any other subdomain is a site of its own.
What you set
The layout, the colours and the corner radius, on the site’s own page in your dashboard. You also set a category. Ads in the same category are preferred but never required, so the slot always fills.
Its public page
Its numbers and its latest clicks one by one, each marked counted or refused, with the reason.

Boards

a site that curates

A site that ranks other people’s products, such as a leaderboard, a directory or a list. It is not another kind of account and it has no signup of its own: you make one out of a site you already hold, in your dashboard. The site stays a site, and its own units and numbers do not change.

What it lists
Products, added by their address or pulled from the library. The words and the picture are never the board’s to write. A product with an ad of its own shows that ad’s words and follows its edits; one without shows what its own page says about it.
What it decides
The order, and when a listing comes off. A rank is a position, with 1 at the top. A listing is retired and brought back, never deleted.
What it gets back
Its name on every one of its ads, as above, and every click on that credit, judged like any other and shown on the board’s own page.
Where its listings run
Across the network, and never on the board’s own site.

A domain that is already a board, whether we curate it or another account runs it, is not handed over by a button. A person does it, once we know it is you.

Rendering it yourself

How to ask, what path 2 hands you, and a unit built out of it: one ad, everything needed to render it, everything needed to disclose it.

The page nonce

one per page view

A short string you invent once per page view and send on every unit request for that page. It is the whole of how the server knows three requests are three slots on one page rather than three pages — there is nothing else it could tell them apart by, and it will not guess.

GET https://plainsun.com/api/v1/unit?site=YOUR_SITE_KEY&nonce=k3f9x2qa
What it may look like
Anything matching [A-Za-z0-9_-]{4,64}. Anything else is ignored — and ignored means treated as no nonce at all, not refused, so a malformed one fails quietly and the page stops being deduplicated.
How long it is a page
10 minutes. After that the rows behind it are purged and the same string is a fresh page, which is why it is per page view and not per URL: hand two readers the same one and the second gets a page deduplicated against somebody else’s.
What it buys
No product twice under one nonce, and at most 8 units. Deduplication is on the product’s domain rather than the ad’s id, because one product listed on two boards is two rows and would otherwise land twice on the same page. On a few hosts where the first path segment is a profile, product_domain carries the handle — x.com/@jack — and deduplication is still on the host it sits on, so two profiles on one host never land on one page either.
When the page is full
200 and {"ad": null, "reason": "max units per page reached"}. A refusal is still a normal response — render your fallback and carry on.

Send nothing and nothing is deduplicated or capped. Every request is its own page as far as the server is concerned, so a three-slot page can fill with the same ad three times. The widget always sends one, so path 1 has this for free. &exclude= takes a comma-separated list of product domains you already have on the page; it is additive help and never authoritative, because a caller firing its requests at once always has a list one step behind.

What the API returns

{
  "ad": {
    "id": 42,
    "title": "Cursor",
    "description": "The AI-first code editor.",
    "image_url": "https://plainsun.com/img/<hash>.webp",
    "product_url": "https://cursor.sh",
    "product_domain": "cursor.sh",
    "source_board": "rankbid.lol",
    "source_url": "https://plainsun.com/go/<signed-credit-token>",
    "category": "devtools",
    "image_is_generated": false
  },
  "go_url": "https://plainsun.com/go/<signed-token>",
  "impression_token": "<signed, single-use, 1h>",
  "disclosure": {
    "required": true,
    "source_text": "Ad · via rankbid.lol",
    "source_url": "https://plainsun.com/go/<signed-credit-token>",
    "network_text": "Daylight",
    "network_url": "https://plainsun.com/"
  },
  "site": {
    "slug": "your-site-example",
    "layout": "card",
    "theme": { "bg": "#ffffff", "fg": "#111111", "accent": "#1a56ff" }
  }
}

image_url is never null: a listing with no usable image gets a generated card in its category colour, flagged by image_is_generated. site.layout and site.theme carry the choices from the site’s own page, so a server-rendered unit can honour them too.

The disclosure is not optional. Both source_text and network_text stay visible, and source_text links to disclosure.source_url when it is set. That link is tracked: it lands on the board with the same ?ref=, and the click is counted for the board and never as a click on the product. It is null for a listing with no board, in which case the text stands alone.

Barebones markup

hand-rolled

A compliant hand-rolled unit, for when you render it yourself. Every placeholder is a field of the response above.

<article class="ad" role="complementary">
  <a href="{{go_url}}" target="_blank" rel="noopener noreferrer sponsored nofollow">
    <img src="{{image_url}}" alt="" width="96" height="96">
    <h3>{{title}}</h3>
    <p>{{description}}</p>
  </a>
  <p class="disclosure">
    <a href="{{source_url}}">{{disclosure.source_text}}</a> ·
    <a href="{{network_url}}">via {{network_text}}</a>
  </p>
</article>

The whole card wraps the link to go_url, and the disclosure line sits outside it — a sibling, never a child, because anchors cannot nest and a nested one silently makes the credit unclickable. That line is the one part of a unit a publisher may not restyle away.

Styling the unit

What the optional stylesheet does, and everything in it you are meant to change. Your own CSS always wins.

The variables

8 of them

Everything the stylesheet decides is a custom property on your placeholder, with the default it ships with. Set any of them anywhere your CSS can reach the unit.

--dl-radius
10px — the card and image corner
--dl-media
140px — the picture height in the stack layout
--dl-bg
#ffffff — the card behind the ad
--dl-fg
#111111 — the title and the body
--dl-muted
#666666 — the description and the disclosure line
--dl-accent
#1a56ff — the letter on a generated image
--dl-border
rgba(0, 0, 0, 0.08) — the card edge
--dl-shadow
0 1px 2px rgba(0, 0, 0, 0.04) — the card lift
[data-daylight] .dl-card {
  --dl-bg: #0f172a;
  --dl-fg: #e2e8f0;
  --dl-muted: #94a3b8;
  --dl-accent: #38bdf8;
  --dl-radius: 14px;
}

The controls on the site’s own page write these for you and the unit carries them as inline custom properties. Nothing in f.css is !important, so a rule of your own beats all of it without one — and a site can be stripped of every stored value from its own page, leaving your stylesheet the only thing styling the unit.

The classes it styles

The widget’s own markup, outermost first. These are the names in f.css, so a rule of yours on any of them lands whether or not you link it.

[data-daylight]
Your placeholder. The widget replaces what is inside it and leaves the element itself alone, so this is where the variables above are declared.
.dl-card
The box: background, border, radius, shadow, and the hover lift. It carries the layout as a modifier — .dl-card--stack, .dl-card--compact.
.dl-unit
The click link, wrapping the image and the text. A grid: image column, then body.
.dl-unit__media
The image well, 96px square on the default layout, full width on a stack.
.dl-fallback
Drawn in place of the image when a listing has none, with .dl-fallback__letter over it in the accent.
.dl-unit__title
One line, clipped with an ellipsis.
.dl-unit__desc
Two lines on a card, one on a compact.
.dl-unit__meta
The disclosure row, outside the link. It holds .dl-unit__source (the board credit) and .dl-unit__network (the Daylight mark).

On the API path these names are yours to choose. The hand-rolled unit above uses its own, so f.css does nothing for it — either copy the class names here and link the file, or write your own CSS and skip it. The one thing neither route may drop is the disclosure row.

f.css in full

optional

All 116 lines of it, read from the file this network serves — not a copy of it. https://plainsun.com/f.css hands a browser these exact bytes.

/* Daylight unit — optional default styles. Publishers may override any of
   these; the CSS variables below are the supported customization surface.
   Loading this file is optional: the widget renders with clean semantic
   markup and works without any styling. */

[data-daylight] {
  --dl-radius: 10px;
  --dl-media: 140px;
  --dl-bg: #ffffff;
  --dl-fg: #111111;
  --dl-muted: #666666;
  --dl-accent: #1a56ff;
  --dl-border: rgba(0, 0, 0, 0.08);
  --dl-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
  display: block;
}

.dl-card {
  background: var(--dl-bg);
  border: 1px solid var(--dl-border);
  border-radius: var(--dl-radius);
  box-shadow: var(--dl-shadow);
  padding: 12px;
  font: 14px/1.35 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.dl-card:hover {
  transform: translateY(-1px);
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
}

/* The card link. The disclosure row is a sibling, not a child: anchors cannot
   nest, and the source credit has to be independently clickable. */
.dl-unit {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: 12px;
  align-items: center;
  color: var(--dl-fg);
  text-decoration: none;
}

.dl-unit__media {
  width: 96px;
  height: 96px;
  border-radius: calc(var(--dl-radius) - 3px);
  overflow: hidden;
  background: #f4f4f4;
  display: flex;
  align-items: center;
  justify-content: center;
}
.dl-unit__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dl-fallback {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, #eef2ff, #e0e7ff);
}
.dl-fallback__letter {
  font-size: 32px; font-weight: 700; color: var(--dl-accent);
}

.dl-unit__body { min-width: 0; }
.dl-unit__title {
  font-weight: 600;
  color: var(--dl-fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dl-unit__desc {
  color: var(--dl-muted);
  margin-top: 2px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.dl-unit__meta {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  color: var(--dl-muted);
  margin-top: 8px;
}
.dl-unit__source, .dl-unit__network {
  color: var(--dl-muted);
  text-decoration: none;
}
.dl-unit__source:hover, .dl-unit__network:hover { text-decoration: underline; color: var(--dl-fg); }

/* ---- Layout variants (dashboard unit editor) ------------------------------
   card    image left, text right (default)
   stack   image above, text below — fits narrow sidebars
   compact text only, no image — fits inline/footer placements            */

.dl-card--stack .dl-unit {
  grid-template-columns: 1fr;
  gap: 10px;
}
/* The band is --dl-media because a rail is not one width. 140px is a natural
   picture in a 300px sidebar and a tall block in a 200px one, and a publisher
   who has the narrow kind should not have to restyle the media box to fix it. */
.dl-card--stack .dl-unit__media {
  width: 100%;
  height: var(--dl-media);
}

.dl-card--compact .dl-unit {
  grid-template-columns: 1fr;
  gap: 2px;
}
.dl-card--compact .dl-unit__desc {
  -webkit-line-clamp: 1;
}

Linking it is optional and the unit is semantic markup without it. If your page sends a content security policy, style-src has to allow plainsun.com for the link to load — the directive people forget, because the script is the tag they were thinking about.

Letting an agent do it

What the third way in actually does, what it will stop and ask you for, and how you know it worked.

What to say

two lines

Any agent that can fetch a URL and edit your files. There is nothing to install, no key to give it and no account of ours for it to hold — the whole instruction is the address of a public document.

Add Daylight ads to this site.
Read https://daylight.ad/SKILL.md and follow it.

If it comes back with a summary rather than the text — most fetch tools do — tell it to read the file raw instead. About a third of that document is a rule about what must not happen, and those are the lines a summary drops first. Its own opening instruction says the same thing, which only helps if it got there.

curl -s https://daylight.ad/SKILL.md

The five steps

in order

What it will work through. Steps two and five are the ones a person doing this by hand skips, and they are the reason the document exists rather than a copy of the snippets above.

1 · Get the three values
The serving host, which it already has; your site key; and the domain the ads will run on. Only the first is something it can know on its own.
2 · Check the registered domain
Before touching your page, because a key installed on a domain it was not registered for fills perfectly and counts nothing.
3 · Choose the placement, write the fallback
Where the unit goes and what sits in the slot when the script cannot load. Both are yours to decide and it is told to ask.
4 · Install
Two tags and a placeholder, or the JSON path where a framework needs it. It knows the three ways to get this wrong that fail silently.
5 · Prove it counts
Two calls: mint a unit for your domain, redeem its token once, read the verdict the network sealed into it. This is the step that makes the document worth having.

What to expect

Three stops, and they are all yours. It cannot make your account, so it will send you to sign up and wait while you hand it the site key. It will ask where the unit goes, because that is your page and your design. And it will write the fallback content with you rather than for you, since that is your markup and it stays on the page for ever if the script is ever blocked.

It ends on a sentence, not a guess. Step five answers counted:true or counted:false, so "it works" is something it can show you rather than something it believes. Expect to be told the first few impressions on the page are yours and its own — they are, and nothing is wrong.

What no agent can check is the last thing worth checking. Open the page yourself, look at the unit, then block the serving host and reload: your fallback content should be sitting there looking intentional. That is the release gate and nothing automates it.

The document is public and plain markdown — /SKILL.md, or /skill if something in your stack refuses a .md address. Nothing on this page is missing from it, and it carries the content security policy block and the per-framework notes this page does not.