Repository navigation
Description
Context
AdCP 3.3 adds the optional account.stable_account_id capability (adcontextprotocol/adcp#8123). A seller that declares it:
- MUST return
account_idon every non-failedsync_accountsprovisioning result; - MUST accept
{ "account_id": "..." }wherever anAccountRefis accepted, including for buyer-declared (natural-key) accounts; - MUST NOT change the id for the life of the account.
Buyers SHOULD send that id instead of the natural key. 3.3 also deprecates the natural key as the reference after provisioning, with removal planned for 4.0 (adcontextprotocol/adcp#7850). The TypeScript SDK shipped the buyer side in adcp-client#3222.
adcp (main, pinned to 3.2.1) has neither side yet.
Buyer side (src/adcp/accounts.py, AccountRegistry.prepare)
The registry already keeps the account_id from sync_accounts (AccountRecord.account_id). prepare() deliberately never swaps it in: "Never infer that a returned handle replaces a natural key." That's correct for a 3.2 seller. Requested:
- When the seller declares
account.stable_account_id: true,prepare()(underauto/strict) rewrites a provisioned natural-key reference to{ "account_id": record.account_id }on every account-scoped task. That covers discovery,sync_creativesand media buys, withsync_accountsprovisioning andlist_accountsfilters keeping their authored references. Without the capability, keep today's behavior: an id returned by a 3.2 seller does not prove it's accepted. - A compatibility override, matching TS
accountReferenceMode: 'natural-key'. - Registry lookup by
account_id, scoped to seller and buyer identity, for webhook and status-repair flows that arrive with only an id. - Note: the existing status gate is correct. It blocks only spend tasks on
payment_requiredand letssync_creativesthrough, which matches the spec. Keep that; the TS SDK has a bug here (adcp-client#3223).
Seller side (src/adcp/decisioning/accounts.py)
NaturalKeyAccounts.resolve rejects any account_id reference with ACCOUNT_NOT_FOUND ("Use the provisioned natural key"). A Python seller therefore can't declare stable_account_id without writing its own store. Requested:
- An opt-in way for
NaturalKeyAccountsto resolve{ account_id }. For example, an optionalloader_by_id(account_id, auth_info)callback, or a store flag that routes ids through the adopter's lookup. It must resolve only accounts the authenticated caller may operate, so an id never crosses principals. When it's configured, the framework (or docs) should prompt the adopter to declareaccount.stable_account_id: trueand returnaccount_idfromupsert_requestresults. - Both forms must reach the same account. State written under the natural key has to be visible under the id and vice versa. The AdCP training agent hit exactly this split while implementing the capability (feat(training-agent): declare account.stable_account_id on v6 tenant routes adcp#8124).
Schema dependency
stable_account_id exists only from the 3.3 beta schemas; this SDK pins 3.2.1. Until it syncs 3.3, read the capability from the raw capabilities payload (or behind 3.3 support), and treat it as absent for 3.2 sellers.
Acceptance
- Buyer: with a seller declaring
stable_account_id: true, afteraccounts.ensure(),get_products,sync_creativesandcreate_media_buycarry{ account_id }. A seller without it still gets the natural key. - Buyer: override keeps natural keys; lookup by
account_idfinds theensure()entry. - Seller:
NaturalKeyAccountswith id resolution configured resolves{ account_id }and the natural key to the same account; another principal's id returnsACCOUNT_NOT_FOUND. - Docs for both sides.
Activity
feat(decisioning): resolve account_id references in NaturalKeyAccount…
bokelley commentedon Nov 8, 2026
Re-scope:
account.stable_account_idis deprecated in AdCP 3.3 (get-adcp-capabilities-response.json,x-deprecated-in: 3.3.0, planned for removal in 4.0). Its guarantee is now universal: every seller MUST returnaccount_idfromsync_accountsonce the account is resolved, accept it on every account-scoped task, and never change it. Sellers SHOULD still sendstable_account_id: truewhile the field exists, and buyers MUST NOT require it before sendingaccount_id. This follows adcontextprotocol/adcp#8474: identity is the brand domain, and an account is itsaccount_id.For SDK 10 (
next), that means:account_id. Don't gate on the capability flag.account_idand accept it everywhere anAccountRefis accepted. Emitaccount.stable_account_id: truefor compatibility.The 9.x line (AdCP 3.2) is unchanged.
🤖 Generated with Claude Code