Skip to content

feat: Implement sync_accounts tool #1029

@bokelley

Description

@bokelley

Summary

Implement the sync_accounts AdCP v3 tool that allows buyer agents to provision and manage advertiser accounts. This is the write counterpart to list_accounts (#1011).

Depends on: #1012 (Agent/Account data model refactor)
AdCP version: v3.0.0-beta.3 (adcp library 3.3.0)

What sync_accounts Does

Buyer agents call sync_accounts to onboard advertiser accounts. The sales agent provisions them, maps to the ad server, and returns status.

Request

class SyncAccountsRequest(AdCPBaseModel):
    accounts: list[Account]       # Accounts to provision
    delete_missing: bool = False  # Deactivate accounts not in list
    dry_run: bool = False         # Preview without applying
    push_notification_config: PushNotificationConfig | None  # Async webhooks

Each account has:

  • house: Brand house domain (e.g., "unilever.com")
  • brand_id: Brand within house portfolio (e.g., "dove")
  • operator: Agency/operator domain (e.g., "groupm.com")
  • billing: Who gets invoiced - "brand", "operator", or "agent"

Response

Returns each account with:

  • account_id: Seller-assigned ID
  • action: "created" | "updated" | "unchanged" | "failed"
  • status: "active" | "pending_approval" | "payment_required" | "suspended" | "closed"
  • credit_limit: Amount and currency
  • payment_terms: e.g., "net_30", "prepay"
  • setup: If further setup needed (URL + message)

Implementation Plan

1. Add _impl() function

# src/core/tools/accounts.py (new file)
from adcp.types import SyncAccountsRequest, SyncAccountsResponse

def _sync_accounts_impl(request: SyncAccountsRequest, context) -> SyncAccountsResponse:
    # 1. Validate agent has permission to manage accounts
    # 2. For each account in request:
    #    a. Check if account exists (by house + brand_id)
    #    b. Create or update Account record
    #    c. Map to ad server advertiser via adapter
    #    d. Set initial status
    # 3. Handle delete_missing (deactivate unlisted)
    # 4. Handle dry_run (validate without persisting)
    # 5. Return response with per-account status

2. Add MCP tool wrapper

# src/core/main.py
@mcp.tool()
def sync_accounts(...) -> SyncAccountsResponse:
    return _sync_accounts_impl(...)

3. Add A2A raw function

# src/core/tools/accounts.py
def sync_accounts_raw(...) -> SyncAccountsResponse:
    return _sync_accounts_impl(...)

4. Adapter interface

# src/adapters/base.py
def sync_advertiser(self, account: Account) -> AdvertiserMapping:
    """Create/update advertiser in ad server."""

GAM adapter: Use AdvertiserService to create/update Companies.

Key Design Decisions

  1. Account matching: Match by house + brand_id combo (unique per tenant)
  2. Approval flow: May need workflow step for pending_approval status
  3. Credit limits: Store locally; ad server may have its own limits
  4. delete_missing: Only affects accounts previously synced by this agent

Files to Modify

File Change
src/core/tools/accounts.py New file: _sync_accounts_impl() + raw function
src/core/main.py MCP tool wrapper
src/core/tools/__init__.py Export raw function
src/adapters/base.py sync_advertiser() interface
src/adapters/gam/adapter.py GAM advertiser creation
src/adapters/mock/adapter.py Mock implementation
tests/unit/test_sync_accounts.py Unit tests

Testing

uv run pytest tests/unit/test_sync_accounts.py -v
uv run pytest tests/unit/test_adcp_contract.py -v

Test cases:

  • Create new account → status "active", action "created"
  • Update existing account → action "updated"
  • dry_run=True → no database changes
  • delete_missing=True → deactivates unlisted accounts
  • Account creation fails → action "failed" with errors
  • Agent permission validation

Metadata

Metadata

Assignees

No one assigned

    Labels

    adcp-v3AdCP v3 spec gapenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions