mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-14 06:47:28 +00:00
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>
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
// Signed, single-use tracking tokens for impressions and clicks. The exchange
|
|
// mints an impression_token when it serves an ad; confirming that impression
|
|
// mints a click_token. Tokens are HMAC-signed so a publisher cannot forge a
|
|
// billable event, and they carry the pricing context needed to settle.
|
|
|
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
|
|
export type TokenKind = "impression" | "click";
|
|
|
|
export interface TokenPayload {
|
|
/** Token kind. */
|
|
k: TokenKind;
|
|
/** request_id the token was minted for. */
|
|
rid: string;
|
|
/** placement id. */
|
|
pid: string;
|
|
/** ad id. */
|
|
aid: string;
|
|
/** campaign id. */
|
|
cid: string;
|
|
/** pricing model of the winning ad. */
|
|
model: "cpm" | "cpc" | "cpa" | "flat";
|
|
/** per-unit charge for the winner at the second-price clearing level. */
|
|
charge: number;
|
|
/** currency code. */
|
|
cur: string;
|
|
/** unix ms expiry. */
|
|
exp: number;
|
|
/** random nonce to keep tokens unique + single-use. */
|
|
n: string;
|
|
}
|
|
|
|
function b64url(input: Buffer | string): string {
|
|
return Buffer.from(input).toString("base64url");
|
|
}
|
|
|
|
function sign(secret: string, body: string): string {
|
|
return createHmac("sha256", secret).update(body).digest("base64url");
|
|
}
|
|
|
|
export function mintToken(secret: string, payload: TokenPayload): string {
|
|
const body = b64url(JSON.stringify(payload));
|
|
return `${body}.${sign(secret, body)}`;
|
|
}
|
|
|
|
export interface VerifyResult {
|
|
ok: boolean;
|
|
reason?: "malformed" | "bad_signature" | "expired";
|
|
payload?: TokenPayload;
|
|
}
|
|
|
|
export function verifyToken(secret: string, token: string, now = Date.now()): VerifyResult {
|
|
const dot = token.indexOf(".");
|
|
if (dot <= 0) return { ok: false, reason: "malformed" };
|
|
|
|
const body = token.slice(0, dot);
|
|
const sig = token.slice(dot + 1);
|
|
const expected = sign(secret, body);
|
|
|
|
const a = Buffer.from(sig);
|
|
const b = Buffer.from(expected);
|
|
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
return { ok: false, reason: "bad_signature" };
|
|
}
|
|
|
|
let payload: TokenPayload;
|
|
try {
|
|
payload = JSON.parse(Buffer.from(body, "base64url").toString("utf8")) as TokenPayload;
|
|
} catch {
|
|
return { ok: false, reason: "malformed" };
|
|
}
|
|
|
|
if (typeof payload.exp === "number" && payload.exp < now) {
|
|
return { ok: false, reason: "expired", payload };
|
|
}
|
|
|
|
return { ok: true, payload };
|
|
}
|