# 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
<?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
<?php
require __DIR__.'/_slots.php';
Adneto::handle();
```

In a template, where the slot should appear:

```php
<?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
```

```php
'slots' => ['token' => env('SLOTS_TOKEN')],
```

3. Configure the client once, in `AppServiceProvider::boot()`:

```php
require_once app_path('Support/adneto.php');

\Adneto::configure([
    'token'     => (string) config('services.slots.token'),
    'cache_dir' => storage_path('app/slots'),
    'endpoint'  => '/l',
]);
```

4. 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:

```php
Route::get('/l', function () {
    \Adneto::handle();
    exit;
})->withoutMiddleware('web');
```

5. In a Blade template (note the unescaped output):

```blade
@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.

```json
{
  "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.

```json
{
  "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.
