| name | build-seller-agent |
| description | Use when building an AdCP seller agent in Go — a publisher, SSP, or retail media network that sells advertising inventory to buyer agents. |
Build a Seller Agent (Go)
Overview
A seller agent receives briefs from buyers, returns products with pricing, accepts media buys, manages creatives, and reports delivery.
When to Use
- User wants to build an agent that sells ad inventory in Go
- User mentions publisher, SSP, retail media, or media network in the context of AdCP
Not this skill: buyer/DSP agents, audience signals (skills/build-signals-agent/), creative rendering (skills/build-creative-agent/)
Before Writing Code
Ask the user — don't guess.
- What kind of seller? Premium publisher (guaranteed, fixed pricing) / SSP (non-guaranteed, auction) / Retail media (both)
- Guaranteed or non-guaranteed?
delivery_type: "guaranteed" vs "non_guaranteed". Many sellers support both.
- Products and pricing. Each product needs: product_id, name, description, publisher_properties, channels (array of channel enums), delivery_type, pricing_options, format_ids, and reporting_capabilities. Use
PublisherPropertySelector entries pointing at publisher_domain values.
- Approval workflow. Instant create returns a media buy such as
status: "active" or status: "pending_creatives". Async create returns the submitted task envelope (status: "submitted", task_id, optional message) and later exposes the confirmed buy via get_media_buys or signed webhooks to push_notification_config.url — see skills/build-webhook-publisher/ for the emission pattern.
- Creative management. Standard (
list_creative_formats + sync_creatives) or none.
Complete Skeleton
adcp.Register wires handler functions to the server. Set only the handlers you support — capabilities are auto-detected. Account resolution and error formatting are automatic.
package main
import (
"context"
"fmt"
"log"
"sync"
"sync/atomic"
"time"
"github.com/adcontextprotocol/adcp-go/adcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
const agentURL = "http://localhost:3001/mcp"
type backend struct {
mu sync.RWMutex
accounts map[string]*adcp.AccountResult
mediaBuys map[string]*adcp.MediaBuyData
creatives map[string]string
delivery map[string]*struct{ Impressions, Clicks int; Spend float64 }
buySeq atomic.Int64
}
var products = []adcp.Product{
{
ProductID: "premium-display", Name: "Premium Display",
Description: "High-impact display placements.",
Channels: []string{"display"}, DeliveryType: "guaranteed",
PublisherProperties: []adcp.PublisherPropertySelector{
{PublisherDomain: "example.com", SelectionType: "all"},
},
PricingOptions: []adcp.PricingOption{
{PricingOptionID: "pd-cpm", PricingModel: "cpm", FixedPrice: adcp.Ptr(15.00), Currency: "USD"},
},
FormatIDs: []adcp.FormatRef{{AgentURL: agentURL, ID: }},
ReportingCapabilities: adcp.ReportingCapabilities{
AvailableMetrics: []{, , },
AvailableReportingFrequencies: []{},
ExpectedDelayMinutes: ,
Timezone: ,
SupportsWebhooks: ,
DateRangeSupport: ,
},
},
}
formats = []adcp.CreativeFormat{
{
FormatID: adcp.FormatRef{AgentURL: agentURL, ID: },
Name: ,
Renders: []adcp.Render{{Width: , Height: }},
Assets: []adcp.AssetSlot{
{ItemType: , AssetID: , AssetType: , Required: ,
AcceptedMediaTypes: []{, }},
},
},
}
{
b := &backend{
accounts: ([]*adcp.AccountResult),
mediaBuys: ([]*adcp.MediaBuyData),
creatives: ([]),
delivery: ([]*{ Impressions, Clicks ; Spend }),
}
log.Fatal(adcp.Serve( *mcp.Server {
server := mcp.NewServer(&mcp.Implementation{Name: , Version: }, )
adcp.Register(server, adcp.Config{
Sandbox: ,
IdempotencyReplayTTL: * time.Hour,
Capabilities: &adcp.CapabilitiesData{
Account: &adcp.AccountCapabilities{SupportedBilling: []{}},
MediaBuy: &adcp.MediaBuyCapabilities{
SupportedPricingModels: []{},
Portfolio: &adcp.PortfolioCaps{PublisherDomains: []{}},
},
},
ResolveAccount: (any, ) {
b.mu.RLock()
b.mu.RUnlock()
domain :=
ref.Brand != { domain = ref.Brand.Domain }
id := fmt.Sprintf(, domain, ref.Operator)
acct, ok := b.accounts[id]; ok { acct, }
,
},
SyncAccounts: ([]adcp.AccountResult, ) {
b.mu.Lock()
b.mu.Unlock()
results := ([]adcp.AccountResult, , (req.Accounts))
_, acct := req.Accounts {
domain :=
acct.Brand != { domain = acct.Brand.Domain }
id := fmt.Sprintf(, domain, acct.Operator)
result := adcp.AccountResult{AccountID: id, Brand: acct.Brand, Operator: acct.Operator, Action: , Status: }
existing, ok := b.accounts[id]; ok { result.Action = ; result.Status = existing.Status }
b.accounts[id] = &result
results = (results, result)
}
results,
},
SyncGovernance: ([]adcp.GovernanceResult, ) {
results := ([]adcp.GovernanceResult, , (req.Accounts))
_, acct := req.Accounts {
govAcct := acct.Account
govAcct == { govAcct = &adcp.GovernanceAccount{Brand: acct.Brand, Operator: acct.Operator} }
results = (results, adcp.GovernanceResult{Account: govAcct, Status: , GovernanceAgents: acct.GovernanceAgents})
}
results,
},
GetProducts: (*adcp.ProductsData, ) {
&adcp.ProductsData{Products: products},
},
CreateMediaBuy: (adcp.CreateMediaBuyResult, ) {
b.mu.Lock()
b.mu.Unlock()
n := b.buySeq.Add()
id := fmt.Sprintf(, n)
pkgs := ([]adcp.PackageStatus, , (req.Packages))
createPkgs := ([]adcp.Package, , (req.Packages))
hasCreatives :=
i, p := req.Packages {
(p.CreativeAssignments) > { hasCreatives = }
pkg := adcp.Package{
PackageID: fmt.Sprintf(, id, i+), ProductID: p.ProductID,
PricingOptionID: p.PricingOptionID, Budget: p.Budget,
StartTime: p.StartTime, EndTime: p.EndTime,
AgencyEstimateNumber: p.AgencyEstimateNumber,
MeasurementTerms: p.MeasurementTerms, PerformanceStandards: p.PerformanceStandards,
CreativeAssignments: p.CreativeAssignments,
}
pkgs = (pkgs, adcp.PackageStatus{Package: pkg})
createPkgs = (createPkgs, pkg)
}
totalBudget
_, p := req.Packages { totalBudget += p.Budget }
status :=
!hasCreatives { status = }
validActions := []{, , , }
status == { validActions = []{, , } }
buy := &adcp.MediaBuyData{
MediaBuyID: id, Status: status, TotalBudget: totalBudget, Packages: pkgs,
Currency: , ValidActions: validActions,
}
b.mediaBuys[id] = buy
_, pkg := pkgs { b.delivery[pkg.PackageID] = &{ Impressions, Clicks ; Spend }{} }
&adcp.CreateMediaBuySuccess{
MediaBuyID: id, Status: status, Packages: createPkgs,
ValidActions: buy.ValidActions, Sandbox: adcp.Bool(),
},
},
GetMediaBuys: (*adcp.GetMediaBuysResponse, ) {
b.mu.RLock()
b.mu.RUnlock()
buys := ([]adcp.MediaBuyData, )
(req.MediaBuyIDs) > {
_, id := req.MediaBuyIDs {
buy, ok := b.mediaBuys[id]; ok { buys = (buys, *buy) }
}
} {
_, buy := b.mediaBuys { buys = (buys, *buy) }
}
&adcp.GetMediaBuysResponse{MediaBuys: buys},
},
ListCreativeFormats: ([]adcp.CreativeFormat, ) {
formats,
},
SyncCreatives: ([]adcp.CreativeResult, ) {
b.mu.Lock()
b.mu.Unlock()
results := ([]adcp.CreativeResult, , (req.Creatives))
_, c := req.Creatives {
action :=
_, exists := b.creatives[c.CreativeID]; exists { action = }
b.creatives[c.CreativeID] =
results = (results, adcp.CreativeResult{CreativeID: c.CreativeID, Action: action, Status: })
}
_, assign := req.Assignments {
_, buy := b.mediaBuys {
i := buy.Packages {
buy.Packages[i].PackageID == assign.PackageID {
buy.Packages[i].CreativeAssignments = (buy.Packages[i].CreativeAssignments, adcp.CreativeAssignment{
CreativeID: assign.CreativeID, Weight: assign.Weight, PlacementIDs: assign.PlacementIDs,
})
buy.Status =
buy.ValidActions = []{, , , }
}
}
}
}
results,
},
GetDelivery: (*adcp.DeliveryData, ) {
b.mu.RLock()
b.mu.RUnlock()
now := time.Now().UTC()
ids := req.MediaBuyIDs
(ids) == { id := b.mediaBuys { ids = (ids, id) } }
deliveries := ([]adcp.MediaBuyDelivery, )
_, mbID := ids {
buy, ok := b.mediaBuys[mbID]
!ok { }
pkgDel := ([]adcp.PackageDelivery, )
_, pkg := buy.Packages {
pkgDel = (pkgDel, adcp.PackageDelivery{PackageID: pkg.PackageID, Spend: , PricingModel: , Rate: , Currency: })
}
deliveries = (deliveries, adcp.MediaBuyDelivery{MediaBuyID: mbID, Status: buy.Status, Totals: adcp.MediaBuyDeliveryTotals{}, ByPackage: pkgDel})
}
&adcp.DeliveryData{
ReportingPeriod: adcp.ReportingPeriod{Start: now.Add( * time.Hour).Format(time.RFC3339), End: now.Format(time.RFC3339)},
MediaBuyDeliveries: deliveries,
},
},
})
adcp.RegisterTestController(server, &adcp.TestControllerStore{
ForceAccountStatus: (*adcp.StateTransition, ) {
b.mu.Lock(); b.mu.Unlock()
acct, ok := b.accounts[accountID]
!ok { , fmt.Errorf() }
prev := acct.Status; acct.Status = status
&adcp.StateTransition{Success: , PreviousState: prev, CurrentState: status},
},
ForceMediaBuyStatus: (*adcp.StateTransition, ) {
b.mu.Lock(); b.mu.Unlock()
buy, ok := b.mediaBuys[mediaBuyID]
!ok { , fmt.Errorf() }
prev := buy.Status
prev == || prev == || prev == { , fmt.Errorf() }
buy.Status = status
&adcp.StateTransition{Success: , PreviousState: prev, CurrentState: status},
},
ForceCreativeStatus: (*adcp.StateTransition, ) {
b.mu.Lock(); b.mu.Unlock()
prev, ok := b.creatives[creativeID]
!ok { , fmt.Errorf() }
b.creatives[creativeID] = status
&adcp.StateTransition{Success: , PreviousState: prev, CurrentState: status},
},
SimulateDelivery: (*adcp.SimulationResult, ) {
b.mu.Lock(); b.mu.Unlock()
buy, ok := b.mediaBuys[mediaBuyID]
!ok { , fmt.Errorf() }
spend
p.ReportedSpend != { spend = p.ReportedSpend.Amount }
_, pkg := buy.Packages {
ds := b.delivery[pkg.PackageID]
ds == { ds = &{ Impressions, Clicks ; Spend }{}; b.delivery[pkg.PackageID] = ds }
ds.Impressions += p.Impressions; ds.Clicks += p.Clicks; ds.Spend += spend
}
&adcp.SimulationResult{Success: , Simulated: []any{: p.Impressions, : p.Clicks, : spend}},
},
SimulateBudgetSpend: (*adcp.SimulationResult, ) {
b.mu.Lock(); b.mu.Unlock()
buy, ok := b.mediaBuys[p.MediaBuyID]
!ok { , fmt.Errorf() }
total
_, pkg := buy.Packages { total += pkg.Budget }
spend := total * p.SpendPercentage
_, pkg := buy.Packages {
total == { }
ds := b.delivery[pkg.PackageID]
ds == { ds = &{ Impressions, Clicks ; Spend }{}; b.delivery[pkg.PackageID] = ds }
ds.Spend += spend * (pkg.Budget / total)
}
&adcp.SimulationResult{Success: , Simulated: []any{: spend, : p.SpendPercentage}},
},
})
server
}))
}
go.mod
module your-seller-agent
go 1.25
require (
github.com/adcontextprotocol/adcp-go/adcp v0.0.0
github.com/modelcontextprotocol/go-sdk v1.5.0
)
Then go mod tidy.
Validation
go run main.go &
npx @adcp/client storyboard run http://localhost:3001/mcp media_buy_seller --json
Fix failures, repeat until all 9 steps pass.
Returning Typed Errors
Handlers can return adcp.NewError for domain-specific errors instead of generic INTERNAL_ERROR:
return nil, adcp.NewError("BUDGET_TOO_LOW", adcp.ErrorOptions{
Message: "Budget $500 is below the $1,000 minimum for video",
Field: "budget",
})
Error codes with auto-recovery: RATE_LIMITED (retry), BUDGET_TOO_LOW / INVALID_REQUEST (revise), ACCOUNT_NOT_FOUND (terminal).
Product Definitions
Each product needs: ProductID, Name, Description, Channels, DeliveryType, PricingOptions, FormatIDs. PublisherProperties is optional — a slice of adcp.PublisherPropertySelector pointing at publisher domains.
Use lowercase pricing models: "cpm", "cpc", "cpcv", not "CPM".
Broadcast/CTV products include business terms:
{
ProductID: "primetime-30s", Name: "Primetime :30 — M-F 8-11pm",
Description: "Primetime 30-second broadcast spots.",
Channels: []string{"broadcast"}, DeliveryType: "guaranteed",
PricingOptions: []adcp.PricingOption{
{PricingOptionID: "unit-30s", PricingModel: "unit", FixedPrice: adcp.Ptr(5000.0), Currency: "USD"},
},
FormatIDs: []adcp.FormatRef{{AgentURL: agentURL, ID: "broadcast-30s"}},
CancellationPolicy: &adcp.CancellationPolicy{
NoticePeriod: adcp.Duration{Interval: 14, Unit: "days"},
CancellationFee: adcp.CancellationFee{Type: "percent_remaining", Rate: 0.25},
},
MeasurementTerms: &adcp.MeasurementTerms{
BillingMeasurement: &adcp.BillingMeasurement{
Vendor: &adcp.BrandReference{Domain: "nielsen.com"}, MeasurementWindow: "c7",
},
MakegoodPolicy: &adcp.MakegoodPolicy{AvailableRemedies: []string{"additional_delivery", "credit"}},
},
PerformanceStandards: []adcp.PerformanceStandard{
{Metric: "viewability", Threshold: 0.70, Standard: "mrc", Vendor: &adcp.BrandReference{Domain: "doubleverify.com"}},
},
}
Storyboards
| Storyboard | Use case |
|---|
media_buy_seller | Full lifecycle — pass this first |
media_buy_non_guaranteed | Auction flow with bid adjustment |
media_buy_guaranteed_approval | IO approval workflow |
media_buy_broadcast_seller | Broadcast/CTV with measurement windows, Ad-ID, delayed delivery |
deterministic_testing | Test controller state machines |
Common Mistakes
| Mistake | Fix |
|---|
Missing IdempotencyReplayTTL on adcp.Config | Required — set to 24*time.Hour. Panics at startup if unset or outside 1h–7d. |
Missing Description on products | Required by schema validation |
Missing publisher_properties, format_ids, or reporting_capabilities on products | Required fields. Use at least one publisher selector, supported format, and reporting capability block. |
sync_governance response key results | Must be accounts |
sync_creatives status "accepted" | Use "approved" — valid: processing, pending_review, approved, rejected, archived |
Empty slices serialize as null | Use make([]T, 0) not var x []T |
| Uppercase pricing model | Use "cpm", "cpc" not "CPM" |
Ignoring buyer's measurement_terms on packages | Echo accepted terms back on confirmed package |
SDK Reference
import (
"github.com/adcontextprotocol/adcp-go/adcp"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
| Function | Usage |
|---|
adcp.Register(server, adcp.Config{...}) | Wire handlers — only set the tools you support. Auto-detects capabilities. |
adcp.Config.IdempotencyReplayTTL | Required. How long you retain idempotency_key responses. Must be 1h–7d; 24h is standard. |
adcp.Config.Capabilities | Optional typed CapabilitiesData — declare account / media_buy / audience_targeting blocks. Filled in automatically if nil. |
adcp.Config.ResolveAccount | Automatic account resolution. Returns ACCOUNT_NOT_FOUND if nil. |
adcp.NewError(code, opts) | Typed error from handlers (BUDGET_TOO_LOW, TERMS_REJECTED, etc.) |
adcp.Serve(createAgent) | HTTP server on :3001/mcp |
adcp.RegisterTestController(server, store) | Add comply_test_controller for storyboard testing |
adcp.AddTool(server, name, desc, handler) | Register custom tools not covered by Config |
The skill contains everything you need. Do not read additional docs before writing code.