logicsrc/packages/agentad/src/builders.ts
Anthony Ettinger 7d62e3b661
Some checks failed
CI / build (push) Has been cancelled
test / test (push) Has been cancelled
feat(agentad): AgentAd Marketplace PRD + reference exchange (M5)
Add the AgentBBS M5 "AgentAd marketplace" spec and a working reference
implementation built on the existing @logicsrc/schemas AgentAd contracts.

- docs/agentad-marketplace.md: two-sided exchange PRD (buy/sell sides,
  match -> auction -> pace -> serve -> meter -> settle, CoinPay settlement,
  AgentBBS as reference publisher, milestones M5.0-M5.5).
- packages/agentad (@logicsrc/agentad): reference exchange
  - builders that emit schema-valid, always-disclosed ad/campaign/placement docs
  - HMAC-signed, single-use impression/click tracking tokens
  - AgentAdExchange: targeting/format/category matching, second-price auction,
    budget pacing + daily caps, frequency capping, token-driven metering
  - pluggable settlement (InMemorySettlement) that can't overspend escrow
  - runtime validation against the canonical agentad-*.schema.json
- 22 vitest cases (builders, tokens, full serve/meter/settle lifecycle).
- Wire package into root build; link the PRD from README + docs/agentad.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:29:36 +00:00

90 lines
2.8 KiB
TypeScript

// Convenience builders that produce schema-valid AgentAd documents with sensible
// LogicSRC defaults. Every builder fills `type`/`version` and — for ads — enforces
// the mandatory disclosure contract so an undisclosed ad can never be constructed.
import type { Ad, AdRequest, Campaign, Placement } from "./types.js";
import { assertValid } from "./validate.js";
export const AGENTAD_VERSION = "0.1";
let counter = 0;
function autoId(prefix: string): string {
counter += 1;
return `${prefix}-${Date.now().toString(36)}-${counter.toString(36)}`;
}
export type NewAd = Omit<Ad, "type" | "version" | "id" | "disclosure"> & {
id?: string;
version?: string;
disclosure?: Partial<Ad["disclosure"]>;
};
export function createAd(input: NewAd): Ad {
const ad: Ad = {
type: "agentad.ad",
version: input.version ?? AGENTAD_VERSION,
id: input.id ?? autoId("ad"),
...stripMeta(input),
disclosure: {
sponsored: true,
label: input.disclosure?.label ?? "Sponsored",
...(input.disclosure?.advertiser_name
? { advertiser_name: input.disclosure.advertiser_name }
: {})
}
};
return assertValid("agentad-ad", ad);
}
export type NewCampaign = Omit<Campaign, "type" | "version" | "id"> & {
id?: string;
version?: string;
};
export function createCampaign(input: NewCampaign): Campaign {
const campaign: Campaign = {
type: "agentad.campaign",
version: input.version ?? AGENTAD_VERSION,
id: input.id ?? autoId("cmp"),
status: input.status ?? "draft",
...stripMeta(input)
};
return assertValid("agentad-campaign", campaign);
}
export type NewPlacement = Omit<Placement, "type" | "version" | "id"> & {
id?: string;
version?: string;
};
export function createPlacement(input: NewPlacement): Placement {
const placement: Placement = {
type: "agentad.placement",
version: input.version ?? AGENTAD_VERSION,
id: input.id ?? autoId("plc"),
...stripMeta(input)
};
return assertValid("agentad-placement", placement);
}
export type NewAdRequest = Omit<AdRequest, "type" | "version"> & { version?: string };
export function createAdRequest(input: NewAdRequest): AdRequest {
const request: AdRequest = {
type: "agentad.ad_request",
version: input.version ?? AGENTAD_VERSION,
...stripMeta(input)
};
return assertValid("agentad-ad-request", request);
}
// Drop the builder-only override keys so they don't leak into the document and
// trip additionalProperties:false during validation.
function stripMeta<T extends Record<string, unknown>>(input: T): Omit<T, "version" | "id" | "disclosure" | "status"> {
const { version, id, disclosure, status, ...rest } = input as Record<string, unknown>;
void version;
void id;
void disclosure;
void status;
return rest as Omit<T, "version" | "id" | "disclosure" | "status">;
}