Skip to content

feat(decisioning): a Google Ad Manager upstream helper (credentials, cached client, pinned API version) #1786

Closed as not planned

Description

@bokelley

Every seller that sells Google Ad Manager inventory through the SDK writes the same ~150 lines before its first GAM call:

  • build googleads credentials from either an OAuth refresh token (oauth2.GoogleRefreshTokenClient) or a service-account key (google.oauth2.service_account.Credentials with the https://www.googleapis.com/auth/dfp scope, wrapped in oauth2.GoogleCredentialsClient);
  • create one AdManagerClient per network, cache it, and give it a SOAP timeout;
  • pin the Ad Manager API version in one place, so a googleads upgrade does not silently move every service to a new version;
  • route sandbox and mock accounts to a mock upstream instead of Google.

The SDK already owns the last part: PlatformHandler.upstream_url, upstream_for and adcp.decisioning.upstream route a sandbox account to get_mock_upstream_url, and upstream_url's docstring names GAM as an example. But the GAM client itself is left to each seller, so the mock routing and the real client are wired by hand.

Example

The Scope3 embedded sales agent's copy:

  • src/adapters/gam/auth.py (GAMAuthManager: OAuth or service account, from a key file or inline JSON);
  • src/adapters/gam/client.py (GAMClientManager: cached AdManagerClient per network, SOAP timeout, get_service(name, timeout_seconds=...), health check, reset_client());
  • src/adapters/gam/api_version.py (GAM_API_VERSION = "v202608", with an architecture test that no GAM client passes its own version).

The prebid salesagent this was forked from carries the same code.

Suggested shape

An optional extra (adcp[gam]) with, for example:

from adcp.decisioning.upstream.gam import GamCredentials, gam_client

client = gam_client(
    network_code="123",
    credentials=GamCredentials.service_account(key_json) | GamCredentials.oauth(client_id, client_secret, refresh_token),
    api_version="v202608",      # required; one place to move it
    soap_timeout_seconds=60,
)
service = client.service("OrderService")

It would be cached per (network, credentials), honor upstream_for / the mock upstream URL for sandbox accounts, and expose reset() for credential rotation. Optional and low priority: it removes duplicated code across GAM sellers rather than unblocking anything.

Activity

  1. bokelley commented on Nov 2, 2026

    @bokelley
    ContributorAuthor

    Closing as out of scope for the SDK. Thanks for writing it up.

    The SDK stays protocol-level and vendor-neutral. An adcp[gam] extra would make one ad server's client (and its googleads / API-version churn) part of the SDK's surface and release cycle, and invite the same for every other ad server.

    The vendor-neutral half is already here: PlatformHandler.upstream_url, upstream_for and adcp.decisioning.upstream route sandbox and mock accounts to the mock upstream. A seller's GAM client should plug into that.

    The GAM-specific part (credentials, a cached AdManagerClient per network, the pinned API version) fits best in the seller (ESA's src/adapters/gam/). If several GAM sellers want to share one copy, a small standalone package such as adcp-gam would do that without tying the SDK to GAM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions