Skip to content
adscapi

Governance

Governor is the trust layer that lets an agent fire conversions without an open hose. Kill switch, approval gate, per-platform disables, a dispatch-rate cap, and an audit trail.

Why

adscapi fires events. It does not spend ad money. An unsupervised agent can still flood destinations or send to a platform you have turned off. Gate first, then pass the result into track.

ts
import { createAdscapi, Governor, auditTrail } from 'adscapi';

const { entries, sink } = auditTrail();
const gov = new Governor({
  disabledPlatforms: ['reddit'],
  requireApproval: true,
  maxEventsPerWindow: { limit: 100, windowMs: 60_000 },
  auditSink: sink,
});

const ads = createAdscapi();
const g = gov.gate(event, { approved: true, approver: 'ops@example.com' });
if (g.allow) {
  await ads.conversions.track(event, { destinations: g.destinations });
}

gate never throws. Order: kill switch → approval → rate cap → allow (with per-platform disables applied on the allow path).

Policy

ts
type GovernancePolicy = {
  killSwitch?: boolean;
  disabledPlatforms?: string[];
  requireApproval?: boolean;
  maxEventsPerWindow?: { limit: number; windowMs: number };
  auditSink?: (entry: AuditEntry) => void;
};
  • killSwitch: true — block everything. Reason: kill switch is on.
  • requireApproval: true — block unless you pass { approved: true }. An explicit { approved: false } also blocks.
  • disabledPlatforms — still allow: true, but destinations is { reddit: false, … }. track skips those keys.
  • maxEventsPerWindow — in-memory token bucket. Caps are dispatch-rate caps, not spend caps. A blocked kill-switch or approval does not consume a token.

A default new Governor() allows every event and returns { destinations: {} }.

Audit trail

auditTrail() is a tiny in-memory collector you can pass as auditSink. Actions: allowed · blocked-killswitch · blocked-platform · blocked-approval · blocked-ratecap.

ts
const trail = auditTrail();
const gov = new Governor({ disabledPlatforms: ['reddit'], auditSink: trail.sink });
gov.gate(event);
console.log(trail.entries);

A throwing auditSink never breaks the gate. The in-memory buffer keeps the newest 10,000 entries and drops the oldest.

Notes

  • The rate cap is in-process only. On Cloudflare Workers there is no shared memory across isolates — use a Durable Object if you need a coordinated cap.
  • destinations only carries false for disabled platforms. Omitted keys stay active. That is what track expects (opts.destinations?.[key] !== false).
  • adscapi does not persist the trail. Wire auditSink to your own store if the process can restart.

Related