Skip to main content

ADCPOrchestrator - Type-Safe ADCP Operations

What We Built

A production-ready, type-safe orchestrator for ADCP (Ad Campaign Data Protocol) operations that: ✅ Provides full TypeScript type safety ✅ Abstracts webhook vs immediate completion ✅ Handles operation/action lifecycle automatically ✅ Standardizes error handling ✅ Makes ADCP operations feel like regular async functions

Quick Start

import { ADCPOrchestrator } from './services/adcp-orchestrator'

const orchestrator = new ADCPOrchestrator(pool, adcpClient)

// Fully typed - autocomplete works!
const result = await orchestrator.syncCreatives(
  { creative_ids: ['c1', 'c2'] },
  { customerId: 123, brandAgentId: 456, ... }
)

// TypeScript knows the response structure
console.log(`Synced: ${result.summary.succeeded}/${result.summary.total}`)

Features

1. Full Type Safety

// Input validation at compile time
const result = await orchestrator.createMediaBuy(
  {
    po_number: "PO-123",
    start_date: "2025-11-01",
    end_date: "2025-12-31",
    product_ids: ["prod_1"],
    total_budget: 10000,
    // creative_idz: [] // ← TypeScript error: property doesn't exist
  },
  context,
);

// Output is typed
if (result.status === "completed") {
  console.log(result.media_buy_id); // TypeScript knows this exists
}

2. Webhook vs Immediate Abstracted

// Uses webhook internally - waits for callback
const sync = await orchestrator.syncCreatives(...)

// Completes immediately - no webhook
const products = await orchestrator.getProducts(...)

// You don't think about webhooks!

3. Automatic Operation Tracking

// Automatically creates:
// - Operation record in PostgreSQL
// - Action record in PostgreSQL
// - Handles lifecycle (pending → in_progress → completed/failed)
// - Updates on errors
// - Tracks via operation ID

const result = await orchestrator.syncCreatives(...)
console.log(result.operationId) // Can check in dashboard

4. Standardized Error Handling

const result = await orchestrator.createMediaBuy(input, context);

if (result.status === "failed") {
  console.error("Error:", result.error); // TypeScript knows error exists
  // Operation/action already marked as failed in DB
}

Available Methods

syncCreatives() - Sync creatives to sales agent

Type: Webhook-based (waits for callback)
const result = await orchestrator.syncCreatives(
  {
    creative_ids: string[],
    target_agent_ids?: string[],
  },
  context
)

// Returns:
{
  operationId: string,
  status: 'completed' | 'partial' | 'failed',
  results: Array<{
    creative_id: string,
    agent_id: string,
    status: 'synced' | 'failed',
    error?: string
  }>,
  summary: {
    total: number,
    succeeded: number,
    failed: number
  }
}

createMediaBuy() - Create and activate media buy

Type: Webhook-based (waits for activation)
const result = await orchestrator.createMediaBuy(
  {
    po_number: string,
    start_date: string,
    end_date: string,
    product_ids: string[],
    total_budget: number,
    creative_ids?: string[],
  },
  context
)

// Returns:
{
  operationId: string,
  status: 'completed' | 'failed',
  media_buy_id?: string,
  buyer_ref?: string,
  error?: string
}

getProducts() - Query available products

Type: Immediate (no webhook)
const result = await orchestrator.getProducts(
  {
    category?: string,
    formats?: string[],
  },
  context
)

// Returns:
{
  operationId: string,
  products: Array<{
    product_id: string,
    name: string,
    description?: string,
    category: string,
    formats: string[],
    pricing?: {
      cpm: number,
      min_budget?: number
    }
  }>
}

Context Parameter

All methods require a context object:
const context = {
  customerId: number, // Customer ID
  brandAgentId: number, // Brand agent ID
  initiatedBy: string, // Who started this (user ID, system name)
  targetAgentId: string, // Sales agent MCP ID
  targetAgentName: string, // Human-readable agent name
};

Usage in API Endpoints

// routes/sync-creatives.ts
app.post("/api/sync-creatives", async (req, res) => {
  const orchestrator = new TypedOrchestrator(pool, adcpClient);

  const result = await orchestrator.syncCreatives(
    {
      creative_ids: req.body.creativeIds,
    },
    {
      customerId: req.user.customerId,
      brandAgentId: req.body.brandAgentId,
      initiatedBy: req.user.id,
      targetAgentId: req.body.targetAgentId,
      targetAgentName: req.body.targetAgentName,
    },
  );

  res.json({
    success: result.status === "completed",
    operationId: result.operationId,
    summary: result.summary,
  });
});

Adding New Typed Methods

Want to add a new tool? Here’s the pattern:
// 1. Define input/output types
export interface ActivateSignalInput {
  signal_id: string
  media_buy_id: string
}

export interface ActivateSignalOutput {
  operationId: string
  status: 'completed' | 'failed'
  error?: string
}

// 2. Add method to TypedOrchestrator
async activateSignal(
  input: ActivateSignalInput,
  context: OperationContext,
): Promise<ActivateSignalOutput> {
  const operationId = `op_${this.generateId()}`
  const actionId = `act_${this.generateId()}`

  // Create operation
  await this.operationsService.createOperation({
    id: operationId,
    type: 'media_buy.create' as OperationType,
    ...
  })

  // Create action
  await this.actionsService.createAction({
    id: actionId,
    operation_id: operationId,
    action_type: 'activate' as ActionType,
    fulfillment_method: 'api' as FulfillmentMethod, // or 'webhook'
    ...
  })

  // Call ADCP tool
  const result = await this.adcpClient.callTool({
    name: 'activate_signal',
    arguments: input as Record<string, unknown>,
  })

  // Handle response...
  return { operationId, status: 'completed' }
}

// 3. Use it!
const result = await orchestrator.activateSignal(
  { signal_id: 's1', media_buy_id: 'mb1' },
  context
)

Testing

# Run the test script
npx tsx scripts/test-typed-orchestrator.ts

# Watch operations in real-time
open http://localhost:8080/electric-operations-dashboard.html

Files

  • Implementation: src/services/typed-orchestrator.ts (305 lines)
  • Test script: scripts/test-typed-orchestrator.ts
  • This doc: TYPED_ORCHESTRATOR_README.md

Benefits Over Manual ADCP Calls

AspectManualTypedOrchestrator
Type safetyNoneFull TypeScript
Operation trackingManual DB callsAutomatic
Error handlingManual try/catchStandardized
Webhook waitingManual pollingBuilt-in
Code lines50+ per call5-10 per call
AutocompleteNoYes
Refactoring safetyManual searchTypeScript refactor

Integration with Electric Dashboard

All operations created by TypedOrchestrator automatically show up in the Electric dashboard with real-time updates!
# Start dashboard
open http://localhost:8080/electric-operations-dashboard.html

# Run typed operation
const result = await orchestrator.syncCreatives(...)

# Watch it appear in dashboard in real-time!

Next Steps

  1. ✅ Use TypedOrchestrator for all ADCP calls
  2. 📝 Add typed methods for remaining tools
  3. 🧪 Write integration tests
  4. 📦 Consider extracting to @adcp/typed-orchestrator package
  5. 🤝 Propose typed tool support to ADCP project

Status: ✅ Production ready! Type-safe ADCP operations with automatic lifecycle management.
I