# Pulse
> The sponsorship arm of GameOn, a grassroots youth football software ecosystem.
> One brand at a time sponsors the GameOn apps outright. Pulse is also the
> measurement stack behind that inventory, and the home of the GameOn Sales
> Agent, which transacts the inventory over the Ad Context Protocol.
Site: https://pulse.gameon.futbol (public: `/`, `/how-it-works`, `/docs`).
Publisher domain: https://gameon.futbol
Inventory: sponsor lockup, banner and MREC placements inside the Sub Manager
apps for iOS and Android. Sole occupancy for the booked window: no auction, no
rotation, no competitor beside you. Sold as time on screen, not as impressions.
Ads are served first-party. There are no ad SDKs, no third-party tags or pixels,
and no user, device or event data leaves GameOn.
Two audiences read this file, and the sections below are split accordingly.
A **buying agent** wants the AdCP endpoint. A **GameOn product** embedding the
pixel or the ad units wants the tracking and delivery endpoints.
## Buying agents (AdCP)
The GameOn Sales Agent implements the Ad Context Protocol over MCP with the
streamable-HTTP transport. Methods: `initialize`, `ping`, `tools/list`,
`tools/call`.
- Endpoint: https://assets.gameon.futbol/functions/v1/adcp
(infrastructure alias: https://etrspsphgbkapczqxboy.supabase.co/functions/v1/adcp)
- Publisher authorization: https://gameon.futbol/.well-known/adagents.json
- `adcp_version`: `3.2`. `supported_versions`: `3.1`, `3.2`.
- Tools: `get_adcp_capabilities`, `list_products`, `get_products`,
`list_creative_formats`, `get_availability`, `create_media_buy`,
`update_media_buy`, `get_media_buys`, `get_media_buy_delivery`,
`sync_creatives`, `get_task_status`.
- Flexible-window availability: `list_products` accepts
`criteria.offer_filters.availability_horizon` and answers with
time-dimensioned forecast points carrying `availability_status`, computed from
every booking eligibility constraint, not competing holds alone. Declared as
the `media_buy.availability_horizon` capability.
- Human review: `create_media_buy` returns a **pending proposal**, never a
committed buy. Every booking and every creative is reviewed by a person inside
a published 24-hour window. An unanswered proposal auto-releases; we never
auto-approve.
- Reporting: aggregate counts only. Viewable time is measured first-party
against the MRC threshold (at least 50% of the lockup's own pixels in view for
at least one continuous second). Pulse is not MRC accredited, and says so.
Where a placement is not measurable, the figure is reported as null, never as
zero.
- Declined at review: gambling, alcohol, vaping and tobacco. The audience
includes junior-football families.
## Privacy contract
- No PII collected or stored.
- No IP address retention (country derived at edge from CF header when present,
otherwise from a client-supplied region hint — never from IP lookup).
- No User-Agent retention (used only for bot filtering at request time).
- No cookies, no fingerprints, no cross-site identifiers.
- Raw events retained 7 days, then aggregated and purged.
- Apple privacy disclosure not required (no tracking, no personal data).
## What we store per event
- `source` (host, e.g. `gameon.futbol`)
- `path` (URL path, query/hash stripped, max 500 chars)
- `event` (e.g. `pageview`, `ad_click`)
- `ref_category` (`direct` | `internal` | `search` | `social` | `other`)
- `country` (ISO-3166 alpha-2, derived at edge)
- `meta` (optional, strict allow-list: `surface`, `sponsor_id`, `artifact`)
## Endpoints
Base: https://etrspsphgbkapczqxboy.supabase.co/functions/v1
(Custom domain alias: https://assets.gameon.futbol/functions/v1)
### GET /logo-transparent
1x1 transparent PNG beacon. Logs a heartbeat. Always returns the pixel,
even if logging fails. No request body. CORS open.
Use as: `
`
### POST /pulse-event
Typed event ingestion. JSON body:
```
{
"source": "gameon.futbol", // required, host
"path": "/pricing", // optional, default "/"
"event": "pageview", // optional, default "pageview"
"ref": "https://google.com/..." // optional
}
```
Response: `{ "ok": true }`. Bots filtered server-side.
### Share events
Share buttons across our products post a `share_intent` event so we can
see untracked reach without recording what was shared or to whom.
```
POST /pulse-event
{
"source": "gameon.futbol",
"event": "share_intent",
"meta": {
"surface": "lineup", // required slug: lineup | stats | app_invite | sponsor_card | ...
"sponsor_id": "uuid-or-omit", // optional, when the artifact carries a sponsor mark
"artifact": "png" // optional: png | link | text
}
}
```
Use `share_completed` only when the host platform reports a confirmed
completion (rare on iOS). `meta` is hard-capped server-side to the three
keys above — anything else is dropped.
### GET /get-ads?formats=banner,large
Returns published ads for mobile/web rendering. Cache 30 days client-side.
Two ways to call:
**Simple (GET):**
- `formats` — CSV of formats the caller wants (`banner`, `large`). The
server picks the best native asset per format and NEVER cross-serves
(no 300x250 in a banner slot). Single `format=banner` still works.
**Slot manifest (POST, preferred for mobile):** declare every slot you'll
render in one call. The server returns one ad per slot, pre-paired by
brand where possible.
```
POST /get-ads
Content-Type: application/json
{
"slots": [
{ "id": "feed_top", "format": "large", "width": 600, "height": 500, "dpr": 2, "placement": "feed" },
{ "id": "feed_inline1", "format": "banner", "width": 640, "height": 100, "dpr": 2, "placement": "feed" },
{ "id": "feed_inline2", "format": "banner", "width": 640, "height": 100, "dpr": 2, "placement": "feed" }
],
"audience": {
"team_tier": "competitive",
"coach_tier": "developing",
"match_result": "win", // win | loss | draw | none, optional
"country": "AU", // ISO-3166 alpha-2, optional
"sponsor_id": "..." // optional anchor for companion pairing
}
}
```
Rules:
- `slots` required, 1–8 entries. `id` is caller-defined and echoed back.
- `width`/`height` optional. If both provided, the server validates the
aspect against the format (banner 6.4:1, large 1.2:1, ±10%) and returns
`error: "aspect_mismatch"` for that slot rather than serving the wrong asset.
- `dpr` optional 1–3 (default 2). Recorded for future hi-res variants.
- `placement` optional, alphanum+`_-`, max 64 chars. Logged on impression.
- `audience.match_result`/`country` currently logged only; future targeting.
- Same ad will not be returned twice in one response.
Response shape (both modes):
```
{
"served_at": "...",
"slots": { "": { "format": "...", "ad": { ... } | null, "error"?: "..." } },
"by_format": { "banner": [ ... ], "large": [ ... ] },
"ads": [ ... ] // flat list, kept for old clients
}
```
When multiple formats are requested the server pairs companion ads
automatically so a brand can fill both slots in one round-trip without
the client passing `sponsor_id` back.
### GET /ad-click/:adId?to=
302 redirects to `to` and logs `ad_click`. Validates URL is http(s).
### GET /get-sponsors
Returns pitch sponsors (logo_url normalised to absolute https URL).
## Implementation principles
- Failsafe: tracking endpoints never block or error the host page.
- Lightweight: pixel beacon is ~70 bytes; event endpoint is a single fetch.
- Privacy-by-design: no client-side state, no SDK to install.
- Server-side aggregation (daily cron, 2am UTC) to bypass row limits.