AbuseZero

Protect a benefit end to end

Create a benefit, evaluate every claim, act on the result and record what you granted.

This guide protects one benefit, a free trial, from the first claim to the recorded outcome. Each step shows the AbuseZero Node.js SDK first and the AbuseZero REST API call second. For the ideas behind each step, see Benefits and Evaluations.

Before you begin

Install the SDK

npm install abusezero

Create one client and reuse it for every request. Keep it in server code only.

lib/abusezero.ts
import { AbuseZero } from "abusezero/server";

export const abusezero = new AbuseZero({
  key: process.env.ABUSEZERO_SECRET_KEY!,
  outage: "throw",
});

outage decides what your code receives if AbuseZero can't be reached. This guide uses "throw". Handle outages and retries explains the other choices.

Create the benefit

A benefit is the thing you protect. Its key is how every evaluation refers to it. This benefit uses the Balanced protection level, can be claimed once per user, and waits for you to report each outcome.

import { abusezero } from "./lib/abusezero";

const benefit = await abusezero.createBenefit({
  name: "Pro trial",
  key: "pro_trial",
  policy_key: "balanced",
  protection_mode: "protect",
  config: {
    eligibility: { kind: "once" },
    outcome_tracking: "report_outcome",
  },
}, "create-benefit-pro-trial");
Response
{
  "object": "benefit",
  "id": "ben_6f1d2c3b-4a5e-4f60-8b7c-9d0e1f2a3b4c",
  "revision": 1
}

The REST API call is POST /v1/benefits, and the SDK method is createBenefit(). The AbuseZero CLI runs the same action as abusezero benefits create, and the AbuseZero MCP server as abusezero_create_benefit. You can also create benefits on the Benefits page of the dashboard. Every setting is described in the Benefits reference.

Assign a stable claim ID

Every claim needs a claim_id: 1 to 128 printable characters, no spaces. Use the ID your app already has for the request, such as a trial request ID. Keep the same claim_id for every retry of the same claim.

Every evaluation also needs an Idempotency-Key. Use the same key only for an exact retry of the same request. A different request with the same key returns IDEMPOTENCY_CONFLICT.

Evaluate the claim

Call AbuseZero before you grant anything, with the SDK's evaluate() or POST /v1/evaluations. Send every identifier you have; Send identifiers covers each one. AbuseZero checks each one against your workspace's full claim history, and the response tells you which inputs it received.

lib/evaluate-trial.ts
import { abusezero } from "./abusezero";

export type TrialClaim = {
  claimId: string;
  userId: string;
  email: string;
  emailVerified: boolean;
  ip?: string;
  deviceToken?: string;
};

export function evaluateTrial(claim: TrialClaim) {
  return abusezero.evaluate({
    benefit: "pro_trial",
    claim_id: claim.claimId,
    subject: {
      external_id: claim.userId,
      email: claim.email,
      email_verified: claim.emailVerified,
    },
    context: claim.ip ? { ip: claim.ip } : undefined,
    identifiers: claim.deviceToken ? { device_token: claim.deviceToken } : undefined,
  }, `evaluate:${claim.claimId}`);
}
Response
{
  "object": "evaluation",
  "id": "eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93",
  "result": "allow",
  "risk": { "score": 10, "band": "low" },
  "reason": {
    "code": "default_allow",
    "message": "No rule matched, so the policy's default allowed this claim."
  },
  "outcome": { "state": "awaiting_outcome", "due_at": "2026-10-29T15:10:00.000Z" }
}

Send the visitor's real IP address as your trusted proxy chain reports it. AbuseZero rejects private, documentation and other non-routable addresses, such as the one in the example, with IP_NOT_ROUTABLE. In the SDK, evaluate() returns id, result, risk, reason and outcome, with source: "abusezero". Every field is described in The evaluation object.

For manual checks, the AbuseZero CLI runs the same evaluation as abusezero evaluations create, and the AbuseZero MCP server as abusezero_create_evaluation.

Act on the result

Handle four cases: a skipped evaluation, an error, deny and allow. Grant the benefit only on allow.

lib/start-trial.ts
import { AbuseZeroError } from "abusezero/server";
import { abusezero } from "./abusezero";
import { evaluateTrial, type TrialClaim } from "./evaluate-trial";
import { grantTrial } from "./trials";

export async function startTrial(claim: TrialClaim) {
  let evaluation;
  try {
    evaluation = await evaluateTrial(claim);
  } catch (error) {
    if (error instanceof AbuseZeroError && error.retryable) return { status: "try_again" as const };
    throw error;
  }

  if ("status" in evaluation || evaluation.source === "local_outage") {
    return { status: "not_evaluated" as const };
  }

  if (evaluation.result === "deny") {
    return { status: "denied" as const, reason: evaluation.reason?.code };
  }

  await grantTrial(claim.userId);
  await abusezero.outcome(evaluation.id, { outcome: "granted" }, `outcome:${evaluation.id}`);
  return { status: "started" as const };
}

grantTrial stands for your own code that starts the trial.

  • Skipped: status: "skipped" means no decision was made (skipped evaluations), for example because the workspace is out of credits. Choose your own fallback. See Handle outages and retries.
  • Denied: show the person a neutral message. Keep reason.code for your team. Reason codes lists every code.
  • Allowed: grant the benefit, then report the outcome in the next step.

Report the outcome

This benefit waits for your report, so tell AbuseZero what you granted before outcome.due_at. The code in the previous step already does this after grantTrial, with the SDK's outcome(). The REST API call is POST /v1/evaluations/{id}/outcome; the AbuseZero CLI command is abusezero outcomes report, and the AbuseZero MCP server tool is abusezero_report_outcome.

await abusezero.outcome(evaluation.id, { outcome: "granted" }, `outcome:${evaluation.id}`);

If you couldn't grant the benefit after all, report not_granted with a reason instead. Report outcomes covers every case.

Verify your integration

Run one claim through your flow, then read the evaluation back with GET /v1/evaluations/{id}, or abusezero evaluations get in the AbuseZero CLI.

curl https://api.abusezero.com/v1/evaluations/eval_3f6c2a1e-8b4d-4c7a-9e2f-5d1b7a0c4e93 \
  -H "Authorization: Bearer $ABUSEZERO_SECRET_KEY"

Check three things in the response:

  • result is allow and outcome.state is granted.
  • inputs shows received for every identifier you meant to send.
  • policy.version shows the exact policy version the claim ran under.

Then claim the same trial again for the same user with a new claim_id. The second evaluation is denied, with a reason such as prior_claim_user.

How is this guide?

Last updated on

On this page