Reconcile a shared seller account
Real advertiser accounts are not exclusively managed by one buyer agent. A seller operator, another authorized buyer, automation, or a connected platform can change the same campaigns and creatives while your agent is idle. This lab teaches the buyer convergence loop:- subscribe to
account.change_recorded; - acquire a latest checkpoint before snapshotting;
- enumerate authoritative state;
- let the training seller simulate external creation and modification;
- drain
list_account_changes; and - locally allowlist
repair.task, construct a validated request from the authenticated account and resource identity, and reread the resource.
Experimental. This lab exercises the account change feed, an experimental AdCP 3.2 surface (
account.change_feed) while RFC #6810 remains open. It may change between 3.x releases with at least 6 weeks’ notice. See experimental status.Learning objectives
By the end, you can:- explain why task responses are insufficient on a shared account;
- distinguish current snapshots, durable business changes, notification
wake-ups, and
webhook_activity[]transport diagnostics; - close the multi-read bootstrap race with a
latestcheckpoint; - process creation and modification your agent did not initiate; and
- recover from a missed webhook or
CURSOR_EXPIREDwithout accepting stale state.
Connect to the training seller
After the hosted seller advertisesaccount.change_feed, use a caller-unique API key from the
AgenticAdvertising.org dashboard.
Durable feed state and cursors are principal-scoped, so a shared public key is
not suitable for this exercise.
Exercise
1. Confirm capability and register the wake-up
Callget_adcp_capabilities and require:
sync_accounts settings-update mode with
accounts[0].account.account_id: "acc_luma_shared" and
accounts[0].notification_configs[] to register an HTTPS endpoint for
account.change_recorded. The seller proves control before the subscription
becomes active. Persist the returned subscriber configuration.
2. Acquire C0 and snapshot
Before reading any account resources, call:changes is empty. Then enumerate
list_accounts, get_media_buys, and list_creatives, including every status
and page. A default active-only media-buy read is not a complete baseline.
3. Simulate activity outside your agent
Callcomply_test_controller with scenario: "seed_creative", a new,
run-unique creative_id, and a complete image creative fixture. This
represents the connected platform adding a creative while your buyer agent is
idle; do not call sync_creatives first. A run-unique ID keeps repeated lab
runs independent while preserving the controller’s seed idempotency contract.
The training seller first commits the new creative and account change
record, then emits account.change_recorded to your active endpoint. The
webhook’s notification_id equals its change_id.
4. Drain and repair
Calllist_account_changes with C0. Inspect:
resource.typeandresource.resource_id;origin.kind: "connected_platform";changed_paths;repair.task: "list_creatives"; and- the new returned cursor.
list_creatives for the resource ID and accept that response as
current truth. Do not reconstruct the creative from webhook or change-record
metadata, and never execute a feed-supplied task name or arguments directly.
An unknown repair hint requires a safe full account rescan.
5. Observe an external modification
Using the same creative ID, callcomply_test_controller with
scenario: "force_creative_status", status: "rejected", and a categorical
rejection reason. This represents connected-platform policy review after the
creative was added.
Verify that a second account.change_recorded wake-up arrives even though the
buyer called no creative mutation task. Drain from the cursor returned in step
4 and require a status_changed record with
origin.kind: "connected_platform". Reread list_creatives and confirm that
the authoritative status is now rejected.
6. Prove tail and recovery behavior
Calllist_account_changes again with the new cursor. A caught-up response has
changes: [], has_more: false, and still returns a cursor. Persist it.
Then explain both recovery cases:
- Webhook missed: poll from the persisted cursor; the durable feed closes the gap.
CURSOR_EXPIRED: acquire a new latest checkpoint, rebuild every authoritative snapshot, then drain after that checkpoint. Never silently restart from the oldest retained record and pretend the gap is complete.
Assessment
You pass when you can demonstrate externally initiated creation and modification end to end and correctly answer:- Which surface is authoritative current state?
- Why is
webhook_activity[]not the account change feed? - Why must the cursor exist on an empty response?
- What does
has_more: falsemean when a connected source is unavailable? - Which data must never appear in a change record?