Documentation
Outcome-label connectors
Automate credit_burn, abuse_confirmed, and chargeback labels so outcomes arrive without a human calling the API.
Connectors automate those labels so outcomes arrive without a human calling the API.
Fast labels (credit_burn, multi_account_cluster, abuse_confirmed) mature in hours, not months: they are the pilot proof metric. A production-ready credit-burn connector pattern ships in the open-source examples/label-connector package.
Recipe A: usage metering → credit_burn
Meter credit/infra consumption per account, then drain that queue into feedback with the burned USD value:
// Consumer for your usage/credit-metering queue (per-account aggregates).
meteringQueue.on("credit_usage_bucket", async (bucket) => {
// bucket: { accountId, creditsBurnedUsd, observedAt }
const account = await db.accounts.findById(bucket.accountId);
if (!account.chitmarkEventId) {
log.warn("missing chitmarkEventId", { accountId: bucket.accountId });
return; // signup never stored it: fix the verify-time persistence
}
await chitmark.feedback({
eventId: account.chitmarkEventId,
outcome: "credit_burn",
value: 12.4, // economic magnitude: burn $, chargeback $, or converted first-year ARR
unit: "usd", // scale for value: usd | credits | count (default: usd)
source: "usage_metering",
});
});Recipe B: T&S queue → abuse_confirmed
When an abuse decision lands in your trust-and-safety queue, emit a label keyed the same way:
trustSafetyQueue.on("abuse_action_taken", async (action) => {
// action: { accountId, reason, decidedAt }
const account = await db.accounts.findById(action.accountId);
if (!account.chitmarkEventId) return; // never guess: skip and fix signup
await chitmark.feedback({
eventId: account.chitmarkEventId,
outcome: "abuse_confirmed",
source: "trust_queue",
observedAt: action.decidedAt,
});
});Recipe B2: T&S clear → false_positive
false_positive means a previously attributed abuse signal on this eventId was reviewed and determined not to represent abuse (T&S clear / overturn). It is not "Chitmark challenged or denied a legitimate user" (context.decisionCosts.falseChallenge / falseDeny on verify) and not a generic "external classifier was wrong" unless that correction is about abuse attribution joined on this eventId. Misusing it contaminates labeled feedback.
trustSafetyQueue.on("abuse_cleared", async (action) => {
// action: { accountId, decidedAt }: prior abuse label overturned
const account = await db.accounts.findById(action.accountId);
if (!account.chitmarkEventId) return;
await chitmark.feedback({
eventId: account.chitmarkEventId,
outcome: "false_positive",
source: "trust_queue",
observedAt: action.decidedAt,
});
});Recipe C: billing → chargeback
Stripe webhooks resolve the event id via your stored mapping, never a guess:
stripeWebhook.on("charge.dispute.created", async (evt) => {
// Resolve from the account row, not from the dispute payload.
const eventId = await db.accounts.chitmarkEventIdFor(evt.data.object.customer);
if (eventId)
await chitmark.feedback({
eventId,
outcome: "chargeback",
value: evt.data.object.amount / 100,
unit: "usd",
});
});Delivery semantics
Retry transient failures (5xx / network) with your queue's dead-letter policy. Exact duplicate deliveries derive the same warehouse feedbackId when eventId, outcome, value, unit, and observedAt match (prefer a stable business observedAt plus an Idempotency-Key on retries). Unknown or cross-tenant event ids return 404 unknown_event_id: the edge never acknowledges a label generically.