Documentation Index
Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
Use this file to discover all available pages before exploring further.
Identity Match Frequency-Cap Data Flow
This page describes how frequency-cap state reaches the Identity Match service and how Identity Match consumes it at eligibility time. It defines the data flow only — what crosses the boundary between the impression tracker and the Identity Match service. Internal mechanics (how the impression tracker counts impressions, where policies live, what storage layout the Identity Match service uses, how identities are deduplicated upstream) are buyer-internal concerns and are out of scope here.
The wire spec lives in the TMP specification; the conformance invariants the Identity Match service must satisfy are also normative there. The reference implementation of the Identity Match cap-state store ships in adcp-go/targeting/fcap.
Roles
| Component | Responsibility |
|---|
| Identity Match service | At query time, returns eligible_package_ids — the subset of requested packages the user is not currently capped on (and that pass other eligibility checks). It does not count impressions and does not own fcap policies. |
| Impression tracker | Receives pixel fires, decodes TMPX, applies the buyer’s fcap policies (counting, windowing, multi-identity dedup, whatever the buyer’s policy logic does), and signals “cap fired” to the Identity Match cap-state store on the impression that exhausts a cap. |
| Identity Match cap-state store | Records (user_identity, package) → cap-until entries with TTL. Queried by the Identity Match service at eligibility time. Written by the impression tracker (or a downstream service in its pipeline). |
The split is deliberate: counting impressions, evaluating windows, and deciding when a cap fires are buyer-internal policy concerns that vary across buyers and across campaigns. The Identity Match service stays narrow — it answers “is this user currently capped on this package?” and nothing more. New cap dimensions (advertiser, campaign, creative — see extensions) plug into the same boundary contract without changing the service.
End-to-end flow
1. Identity Match query
publisher → router → Identity Match service
Identity Match looks up cap state for each (identity, package) pair
returns eligible_package_ids + tmpx (HPKE-encrypted resolved identities)
2. Ad serves; creative tracking URL fires pixel with {TMPX}
publisher's player/page → impression tracker
3. Impression tracker decodes TMPX
→ resolved identities + signed package context (seller_agent_url, package_id)
4. Impression tracker applies the buyer's fcap policies
→ counts this exposure against whatever dimensions the buyer caps on
(package, campaign, advertiser, creative, line item, …) for each
resolved identity, using whatever policy logic and storage the buyer
runs internally
5. If this impression exhausts a cap (i.e., it is the last allowed exposure
under one of the buyer's policies), the impression tracker (or a
downstream service in its pipeline) writes a cap-fire entry to the
Identity Match cap-state store:
(user_identity, package) capped until <expireAt>
6. Subsequent Identity Match queries for that user see the cap-state entry
and exclude the package from eligible_package_ids until the entry expires
Steps 1, 2, and 6 cross the wire and are normatively defined in the TMP specification. Steps 3 and 5 cross the impression-tracker → cap-state-store boundary and are defined on this page. Step 4 is buyer-internal — the protocol does not constrain it.
The cap-fire event
When a buyer’s policy evaluation determines that an impression has exhausted a cap, the impression tracker writes a cap-fire entry to the Identity Match cap-state store. Each entry consists of:
| Field | Description |
|---|
user_identity | The resolved identity token (e.g., rampid:abc, id5:def, maid:ghi) the cap fired on. If a single impression resolved to multiple identities and the policy fired on all of them, the impression tracker writes one entry per identity. |
seller_agent_url | The seller agent the package belongs to. Disambiguates identical package_id strings across sellers. |
package_id | The package the cap fired on. |
expire_at | Wall-clock time at which the cap expires. The cap-state store enforces this as a TTL — entries are absent after expire_at. |
A single cap-fire event typically corresponds to one entry; a cap that fires on multiple resolved identities or multiple packages produces one entry per (identity, package) pair, all sharing the same expire_at if the buyer’s policy is the same.
The cap-state store does not record per-impression counts, policy definitions, or window configurations. Its only job is to answer “is this (user_identity, package) currently capped?” The buyer’s policy logic — counting, windowing, choosing dimensions to cap on, deciding when to fire — lives entirely in the impression tracker.
The eligibility query
At query time, the Identity Match service receives a list of identities and a list of candidate packages. For each candidate package, it checks the cap-state store for any matching (identity, package) entry across the user’s identities. If any entry exists, the package is excluded from eligible_package_ids. This is a presence check, not a count.
Cap state is one input to eligibility. The Identity Match service also evaluates audience membership, package active state, audience freshness, and any other inputs the buyer cares about — see the conformance invariants. The cap-state portion of that evaluation is the part this page defines.
Policy updates and cap-state re-evaluation
Cap-state entries are written under whatever fcap policy was in force at cap-fire time. When the buyer’s fcap policies change — a window shortens or lengthens, a max_count rises or falls, a policy is paused or removed, a package is reassigned to a different policy — the existing cap-state entries written under the old policy can become stale. Stale entries either suppress users who should now be eligible (over-suppression) or fail to suppress users who should now be capped (under-suppression).
When a fcap rule changes, the buyer’s policy owner (typically the impression tracker or a service in its pipeline) MUST re-evaluate every cap-state entry the rule applied to and push the appropriate update to the IdentityMatch cap-state store. Two event shapes cover the cases:
| Event | When to push | Effect on cap-state |
|---|
| Delete cap-state | A user’s exposure count under the new policy is below the new max_count, or the policy was removed/disabled, or the package was reassigned away from the policy. | Remove the (user_identity, package) entry — the user is no longer suppressed on that package. |
| Extend cap-state | A user is still over-cap under the new policy, but the new expire_at differs from the existing entry — for example, the window was lengthened (push a later expire_at) or shortened (push an earlier expire_at). | Overwrite the entry with the new expire_at. |
Re-evaluation runs over the buyer’s own counting state (where impression history lives), not over the cap-state store — the cap-state store doesn’t carry counts. The output is the set of delete-or-extend events to apply.
The reference store in adcp-go/targeting/fcap implements extend natively (a second RecordCap for the same (user_identity, field) overwrites the prior expire_at via HSETEX). Delete is a future extension — today, the simplest workaround is to extend with an expire_at already in the past, which causes the entry to be treated as absent at the next query and to be reaped by the backend’s TTL machinery.
Re-evaluation can be expensive when a policy applies to many users. Buyers typically run it asynchronously: enqueue the policy-change event, sweep the affected user population in batches, push delete/extend events incrementally. The protocol does not constrain the cadence — only the eventual consistency requirement that cap-state must converge to what the current policies imply.
Reference implementation
The cap-state store API in adcp-go/targeting/fcap is the reference shape. It exposes two operations:
RecordCap(ctx, userIdentity string, fields []Field, expireAt time.Time) error
IsCapped(ctx, userIdentity string, field Field) (bool, error)
— plus batch variants for both. Field is {SellerAgentURL, PackageID}. The reference store is backed by Valkey 9 hashes, hashed by user identity, with one hash field per (seller_agent_url, package_id) tuple and a TTL set to expire_at. Other backends (Aerospike, DynamoDB, in-memory, anything) are conformant if they satisfy the boundary contract above.
Future extensions
Today the cap-state store is keyed at (user_identity, seller_agent_url, package_id). Future protocol versions may extend the field to additional dimensions — advertiser, campaign, creative, line item — so a buyer can express caps that span multiple packages without writing N entries on every cap-fire. The boundary contract on this page is unchanged by such extensions: the impression tracker writes cap-fire entries; the Identity Match service checks presence at query time.
See also