Adneto integration guide
This guide is written so that an AI coding agent (or a developer) can connect a website to Adneto without further help. Follow it top to bottom.
Adneto delivers ads from the backend. Your server downloads a small JSON manifest, caches it and prints the ad as part of your own HTML. The visitor's browser never contacts Adneto: links, images and measurement all go through your own domain. No third-party JavaScript, no cookies, no visitor identifiers.
1. What you need
| Item | Where to get it |
|---|---|
| Site token | Adneto → Sites → your site. Shown once when the site is created; generate a new one if lost. |
| Slot keys | Adneto → Sites → your site → Slots. A key looks like sidebar. |
| Client file | https://adneto.com/download/client (saved as adneto.php) |
| PHP 8.1+ | No extensions required. cURL is used when present. |
| A writable directory | For the cache. It must not be reachable from the web. |
Treat the token as a secret: keep it in an environment variable or in a config file outside the web root (for example private/config.php returning an array), never in a public repository. The examples below read it with getenv('SLOTS_TOKEN'); use whatever your site already uses for secrets.
Configuration options
Adneto::configure([...]) accepts:
| Option | Default | Meaning |
|---|---|---|
token |
— | Site token. Empty = the client does nothing. |
cache_dir |
— | Writable directory outside the web root. |
endpoint |
/go.php |
Address on your site that calls Adneto::handle(). |
api |
https://adneto.com/api/v1 |
API base. Change it only when testing against another Adneto installation. |
timeout |
3 |
Seconds for one request to Adneto. |
params |
['click' => 'r', 'view' => 'v', 'image' => 'm'] |
Names of the endpoint's query parameters. |
fallback_url |
/ |
Where a click goes when its ad is no longer in the manifest. |
Adneto::refreshNow() downloads the manifest immediately and returns true on success. Normal pages never need it; it is useful for tests and for a cron job if you prefer scheduled refreshes.
2. Rules that must hold
- Neutral names. The address that handles clicks, its parameters and the class or id of any wrapper element you add must not contain the words
ad,ads,banner,sponsor,promooradneto(as whole words or word parts separated by-or_, such asad-boxorside_banner;paddingorloadingare fine). Ad blockers hide such elements. Use names like/go.php,/l,tip-box. - Do not alter the returned HTML. Print it as is. Do not escape it, do not wrap it in a link.
- Do not cache the output of the click endpoint. Pages that contain slots may be cached as usual.
- A failure must never break the page.
Adneto::render()returns an empty string when anything goes wrong; do not add code that throws around it.
3. Plain PHP
Directory layout (the web root is public/):
private/adneto.php ← the client, outside the web root
private/cache/ ← writable, outside the web root
public/go.php ← click endpoint (any neutral name)
public/_slots.php ← shared configuration, included where needed
public/_slots.php:
<?php
require __DIR__.'/../private/adneto.php';
Adneto::configure([
'token' => getenv('SLOTS_TOKEN'), // the site token
'cache_dir' => __DIR__.'/../private/cache',
'endpoint' => '/go.php',
]);
public/go.php:
<?php
require __DIR__.'/_slots.php';
Adneto::handle();
In a template, where the slot should appear:
<?php
require_once __DIR__.'/_slots.php';
$tip = Adneto::render('sidebar');
?>
<?php if ($tip !== ''): ?>
<aside class="tip-box"><?= $tip ?></aside>
<?php endif; ?>
The wrapper is printed only when the slot has content, so an empty slot leaves no empty box.
If _slots.php is reachable by URL it only configures the client and prints nothing, which is harmless. You may also place it outside the web root.
4. Laravel
- Save the client as
app/Support/adneto.php(or anywhere outsidepublic/). - Add to
.envandconfig/services.php:
SLOTS_TOKEN=your-site-token
'slots' => ['token' => env('SLOTS_TOKEN')],
- Configure the client once, in
AppServiceProvider::boot():
require_once app_path('Support/adneto.php');
\Adneto::configure([
'token' => (string) config('services.slots.token'),
'cache_dir' => storage_path('app/slots'),
'endpoint' => '/l',
]);
- Add the click route to
routes/web.php. It must not start a session or set cookies, so keep it out of thewebmiddleware group:
Route::get('/l', function () {
\Adneto::handle();
exit;
})->withoutMiddleware('web');
- In a Blade template (note the unescaped output):
@php($tip = \Adneto::render('sidebar'))
@if ($tip !== '')
<aside class="tip-box">{!! $tip !!}</aside>
@endif
With an empty token the client does nothing, so local development and tests work without configuration.
5. How it behaves
- First request: there is no manifest yet, so the slot is empty. The manifest is downloaded after the response is sent; the slot appears on the next request.
- Refresh: every 5–15 minutes, started by a page request and run after the page output is finished. Only one request refreshes at a time. Under PHP-FPM the visitor does not wait for it at all. Under mod_php or the built-in server the page is sent first, but that one request stays open until the refresh ends (normally well under a second, at most about 8 seconds during an outage); all other requests are unaffected.
- Outage: if Adneto is unreachable, the last downloaded manifest keeps being used. Without any manifest the slot is empty and the page works.
- Empty slot:
render()returns''. Hide your wrapper when it is empty if it has padding or a border. - Images are downloaded to the cache and served from your domain through the endpoint.
- Measurement: the client counts server renders, pixel loads and clicks in a file in the cache directory and sends hourly totals to Adneto on refresh. No IP addresses or cookies are stored.
- Robots are not counted. Requests whose user agent looks like a robot or a command-line tool (including the default user agent of
curlandwget, and an empty user agent) still get the ad, the image and the redirect, but are not recorded. When testing measurement with curl, send a browser user agent:curl -A "Mozilla/5.0 Firefox/130.0" …. - A click on an ad that is no longer in the manifest (for example from a cached page) redirects to
fallback_url, by default your home page.
6. Checklist
Run these checks and report each result. Use a browser user agent for every request (see "Robots are not counted").
- The cache directory contains
manifest.jsonafter two page loads a few seconds apart. - The page source contains the slot HTML, and every
hrefandsrcinside it points to your own domain. - In the slot HTML and your wrapper, no
class,id,hreforsrcvalue containsadneto,ads,banner,sponsororpromo, and none containsadas a word or a part separated by-or_. Check attribute values only; text such aspaddingin styles or an image hash does not count. - Opening the link from the slot redirects (HTTP 302) to the advertiser's address.
- The response of the click endpoint has no
Set-Cookieheader. - Outage test: set
'api' => 'https://invalid.invalid/api/v1', keep the cache, and force a refresh by runningAdneto::refreshNow()once from a temporary script that includes your configuration. It must returnfalse. Then reload the page: the slot still renders from the cache. Restore the setting and delete the script. - Empty-cache test: point
cache_dirat an empty directory together with the invalidapiand load the page twice. The page loads, the slot is empty, and the PHP error log (or your framework's log) has no new entry. Restore both settings. - Within 15 minutes the site detail in Adneto shows "Last connected" and, after some page views with a browser user agent, received statistics.
7. Other technologies: the API
If the site is not written in PHP, implement the same behaviour against the API. Base address: https://adneto.com/api/v1. Authenticate every request with the header Authorization: Bearer <site token>.
GET /manifest
Fetch at most once every 5 minutes; honour ttl (seconds). Send If-None-Match with the last ETag; 304 means nothing changed. On 401 the token is wrong or the site is paused: keep the last manifest and retry later.
{
"v": 1,
"ttl": 600,
"slots": {
"sidebar": {
"schedule": [
{
"from": 1794351600,
"until": 1794610800,
"items": [
{
"id": 12,
"campaign": 5,
"html": "<span class=\"x1a2b3c4\"><a href=\"{{click}}\">…</a><img src=\"{{pixel}}\" alt=\"\"></span>",
"url": "https://advertiser.example/landing",
"images": { "<sha256>": "https://cdn.example/…/<sha256>.webp" }
}
]
}
]
}
}
}
To render a slot:
- Pick the schedule segment where
from <= now < until(Unix seconds). No segment or no items means the slot is empty. - Pick one item at random.
- In
html, replace{{click}}with a URL on your domain that will redirect tourl,{{pixel}}with a URL on your domain that returns a 1×1 GIF withCache-Control: no-store, and each{{img:<sha256>}}with a URL on your domain that serves the cached image. HTML-escape the URLs you insert. - Download each image once, verify that the SHA-256 of the bytes equals the key, and serve it with a long cache lifetime.
- Reject an item whose
htmlcontains<script, anon…=attribute orjavascript:. - The click URL must identify the slot, campaign and creative and look up
urlin the manifest. Never redirect to an address taken from the request.
POST /stats
Send counts when you refresh the manifest. Each row is one slot, campaign, creative and hour.
{
"batch": "32 lowercase hex characters, unique per batch",
"rows": [
{ "hour": 1794391200, "slot": "sidebar", "campaign": 5, "creative": 12, "renders": 120, "views": 100, "clicks": 4 }
]
}
houris the Unix timestamp of the start of the hour (UTC).renders= server-side renders,views= pixel loads,clicks= redirects.2xxmeans accepted; a repeatedbatchis acknowledged and not counted twice, so resend the same batch after a network error.422means the batch is malformed: discard it.- Do not count requests from robots. Do not store or send anything that identifies a visitor.