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— stillallow: true, butdestinationsis{ reddit: false, … }.trackskips 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.
destinationsonly carriesfalsefor disabled platforms. Omitted keys stay active. That is whattrackexpects (opts.destinations?.[key] !== false).- adscapi does not persist the trail. Wire
auditSinkto your own store if the process can restart.
Related
- conversions.track() — pass
g.destinationsinto the fan-out - Observability — query what actually shipped after the gate
- Agent commerce