For an AI agent: the same guide as plain Markdown is at https://adneto.com/en/integration-guide/raw

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

  1. 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, promo or adneto (as whole words or word parts separated by - or _, such as ad-box or side_banner; padding or loading are fine). Ad blockers hide such elements. Use names like /go.php, /l, tip-box.
  2. Do not alter the returned HTML. Print it as is. Do not escape it, do not wrap it in a link.
  3. Do not cache the output of the click endpoint. Pages that contain slots may be cached as usual.
  4. 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

  1. Save the client as app/Support/adneto.php (or anywhere outside public/).
  2. Add to .env and config/services.php:
SLOTS_TOKEN=your-site-token
'slots' => ['token' => env('SLOTS_TOKEN')],
  1. 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',
]);
  1. Add the click route to routes/web.php. It must not start a session or set cookies, so keep it out of the web middleware group:
Route::get('/l', function () {
    \Adneto::handle();
    exit;
})->withoutMiddleware('web');
  1. 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 curl and wget, 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").

  1. The cache directory contains manifest.json after two page loads a few seconds apart.
  2. The page source contains the slot HTML, and every href and src inside it points to your own domain.
  3. In the slot HTML and your wrapper, no class, id, href or src value contains adneto, ads, banner, sponsor or promo, and none contains ad as a word or a part separated by - or _. Check attribute values only; text such as padding in styles or an image hash does not count.
  4. Opening the link from the slot redirects (HTTP 302) to the advertiser's address.
  5. The response of the click endpoint has no Set-Cookie header.
  6. Outage test: set 'api' => 'https://invalid.invalid/api/v1', keep the cache, and force a refresh by running Adneto::refreshNow() once from a temporary script that includes your configuration. It must return false. Then reload the page: the slot still renders from the cache. Restore the setting and delete the script.
  7. Empty-cache test: point cache_dir at an empty directory together with the invalid api and 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.
  8. 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:

  1. Pick the schedule segment where from <= now < until (Unix seconds). No segment or no items means the slot is empty.
  2. Pick one item at random.
  3. In html, replace {{click}} with a URL on your domain that will redirect to url, {{pixel}} with a URL on your domain that returns a 1×1 GIF with Cache-Control: no-store, and each {{img:<sha256>}} with a URL on your domain that serves the cached image. HTML-escape the URLs you insert.
  4. Download each image once, verify that the SHA-256 of the bytes equals the key, and serve it with a long cache lifetime.
  5. Reject an item whose html contains <script, an on…= attribute or javascript:.
  6. The click URL must identify the slot, campaign and creative and look up url in 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 }
  ]
}
  • hour is the Unix timestamp of the start of the hour (UTC).
  • renders = server-side renders, views = pixel loads, clicks = redirects.
  • 2xx means accepted; a repeated batch is acknowledged and not counted twice, so resend the same batch after a network error. 422 means the batch is malformed: discard it.
  • Do not count requests from robots. Do not store or send anything that identifies a visitor.