Background
The AdCP TMP spec (adcp#3359) defines frequency caps using an fcap_keys[] label model — tenant:dimension:value strings (e.g. buyer-acme:campaign:42, buyer-acme:advertiser:13, buyer-acme:creative:8) — with arbitrary buyer-chosen dimensions.
The current adcp-go/targeting/ impl predates this design and uses scalar fields:
ExposureEntry { ImpressionID, PackageID, CampaignID, SourceID, Timestamp }
PackageIdentityConfig.FrequencyRules (per package)
CampaignFreqConfig.FrequencyRules (per campaign)
CheckFrequencyRulesMultiLog(logs, filterHash, isCampaign, ...) — boolean isCampaign flag picks between two scalar paths
This means today the impl supports exactly package-level and campaign-level caps; advertiser, creative, line-item, group, or any other dimension a buyer might want to cap on isn't expressible.
What needs to change
Generalize the data model so the dimension is arbitrary:
- ExposureEntry: carry
fcap_keys []string (or pre-hashed fcap_keys [][8]byte for binary log compactness) instead of separate PackageID / CampaignID fields. Or keep PackageID/CampaignID for backward compat and add fcap_keys as an additional field.
- FrequencyRule: attach to a
fcap_key string rather than implicitly to a package or campaign.
- Engine: filter exposure entries by
fcap_key membership in the entry's fcap_keys[], not by scalar match.
- Binary log format:
exposure_binary.go currently has fixed slots for ImpressionHash / PackageHash / CampaignHash / SourceHash. Either widen entries to variable-size (carry a count + N fcap_key hashes) or version-bump to a different layout. Backward compat plan: v1 = current, v2 = arbitrary fcap_keys, decoder handles both.
- CheckFrequencyRulesMultiLog / CheckFrequencyRulesAggregated: drop the
isCampaign bool parameter, take a single filterHash that matches against any fcap_key on the entry.
Backward compatibility
Existing callers using package_id and campaign_id should continue to work. Suggested approach:
- Keep
PackageID / CampaignID fields on ExposureEntry (deprecate but don't remove)
- Add
FcapKeys []string
- During the transition, treat
PackageID and CampaignID as implicit fcap_keys (pkg:<id> and campaign:<id>) so existing rules keep working
- New rules attached via
fcap_key directly
Why this matters
Without the generalization, buyers can only cap at package and campaign level. The whole point of the label model (per the spec) is that buyers choose dimensions — most DSPs need advertiser-level, creative-level, and flight-level caps. Today buyers running adcp-go's targeting/ package can't express those.
Out of scope for this issue
- The pre-aggregation optimization in #103 — that's orthogonal and should land first; the generalization extends the same data structures it indexes.
- Wire-spec changes — TMP itself doesn't carry fcap_keys on the wire; they're buyer-internal. No protocol coordination needed.
- Multi-tenant isolation rules (tenant prefix requirement) — those are buyer-side enforcement that lives in the SDK, not in
targeting/.
References
Background
The AdCP TMP spec (adcp#3359) defines frequency caps using an
fcap_keys[]label model —tenant:dimension:valuestrings (e.g.buyer-acme:campaign:42,buyer-acme:advertiser:13,buyer-acme:creative:8) — with arbitrary buyer-chosen dimensions.The current
adcp-go/targeting/impl predates this design and uses scalar fields:ExposureEntry { ImpressionID, PackageID, CampaignID, SourceID, Timestamp }PackageIdentityConfig.FrequencyRules(per package)CampaignFreqConfig.FrequencyRules(per campaign)CheckFrequencyRulesMultiLog(logs, filterHash, isCampaign, ...)— boolean isCampaign flag picks between two scalar pathsThis means today the impl supports exactly package-level and campaign-level caps; advertiser, creative, line-item, group, or any other dimension a buyer might want to cap on isn't expressible.
What needs to change
Generalize the data model so the dimension is arbitrary:
fcap_keys []string(or pre-hashedfcap_keys [][8]bytefor binary log compactness) instead of separatePackageID/CampaignIDfields. Or keep PackageID/CampaignID for backward compat and addfcap_keysas an additional field.fcap_keystring rather than implicitly to a package or campaign.fcap_keymembership in the entry'sfcap_keys[], not by scalar match.exposure_binary.gocurrently has fixed slots for ImpressionHash / PackageHash / CampaignHash / SourceHash. Either widen entries to variable-size (carry a count + N fcap_key hashes) or version-bump to a different layout. Backward compat plan: v1 = current, v2 = arbitrary fcap_keys, decoder handles both.isCampaign boolparameter, take a singlefilterHashthat matches against any fcap_key on the entry.Backward compatibility
Existing callers using
package_idandcampaign_idshould continue to work. Suggested approach:PackageID/CampaignIDfields onExposureEntry(deprecate but don't remove)FcapKeys []stringPackageIDandCampaignIDas implicit fcap_keys (pkg:<id>andcampaign:<id>) so existing rules keep workingfcap_keydirectlyWhy this matters
Without the generalization, buyers can only cap at package and campaign level. The whole point of the label model (per the spec) is that buyers choose dimensions — most DSPs need advertiser-level, creative-level, and flight-level caps. Today buyers running adcp-go's
targeting/package can't express those.Out of scope for this issue
targeting/.References
docs/trusted-match/identity-match-implementation.mdx#fcap_keys-label-model