# Fraud and risk

How every referral program's fraud engine scores, holds and asks, and what a person decides.

Every referral program gets the same fraud engine, on every plan. It looks at each
click, signup and sale as it arrives, adds up what it saw into a score from 0 to 100,
and **holds** money when the score is high. It never rejects a sale, never bans a
promoter and never confirms fraud by itself: a person decides, in the dashboard's
Fraud tab or through the API, and every decision is recorded in the activity log.

## What the score does

| Score | What happens |
|---|---|
| Under the hold line (40 by default) | Nothing. The commission waits out the program's usual hold. |
| From the hold line | The commission (and any reward that is not cash) waits 14 more days, so a refund or a chargeback has time to arrive. |
| From the review line (70 by default) | The money is held until a person decides, as a `risk_review` flag in the review queue. |
| Self-referral, a shared card, or payout details just changed | Always asked, whatever the score. |

Each commission says why it waits (`risk` in the API: the score and every reason as a
sentence), on the Commissions tab beside its date. The promoter sees the later date, and
"Under review" while it waits for you; never why.

The lines, the extra days and the rest of the settings below are the program's own:
change them on the program's **Settings** tab, under **Fraud**, or with
[`PATCH /v1/referrals/programs/{id}/risk-policy`](https://boltclaw.ai/docs/api/referrals). A change applies
from the next conversion; what was already assessed keeps its hold.

## What it looks at

Each rule that counts adds its weight and a sentence a person can check: what was seen,
in counts and times, never an address, a card or an email.

| Rule | What it means |
|---|---|
| Self-referral | The customer is the promoter: the same email, the same user in your app, or the same card. |
| Too fast after the click | A signup or sale came sooner after the click than a person could manage (5 seconds for a signup, 15 for a sale, 30 for a shop order, by default). |
| Paid search | The click came from a paid ad on a program that does not allow bidding on your brand, unless its campaign id is on your allowlist. |
| Data-centre click | The click came from a cloud or hosting network, not a home or a phone. |
| Codes without clicks | A promoter's sales keep arriving through a discount code nobody clicked a link for. |
| Code leak | One code is redeemed far more often than its links are clicked, usually because it was posted on a coupon site. |
| Look-alike addresses | The customer's email is one or two characters from other customers' in the program. |
| Throwaway email | The customer signed up with a disposable address. |
| Unusual conversion rate | A promoter converts far below or far above everyone else in the program. |
| Refunds | A high share of a promoter's sales are refunded. |
| New promoter | The promoter joined in the last 90 days, and their earnings wait a little longer while "Hold new partners longer" is on, as it is unless you switch it off. |

A click flood from one address stops being counted at all (60 a minute by default), and
the redirect still answers at once: no check ever slows a visitor down.

**Residential proxies are not caught by any list.** They look like homes because they
are homes. The time from click to conversion, the conversion-rate outliers and the
velocity per address and device are what catch them.

## The click id

The website tag keeps a click id (`bc_click`, 16 letters and digits) beside the referral
code, and `BoltClawReferral.clickId()` returns it, so the engine can tell how long a customer took
from the click to the conversion. Pass it on where you pass the code:

- **Stripe Checkout:** `client_reference_id` as `<code>--<click id>`, which is what
  `BoltClawReferral.clientReferenceId()` gives (Payment Links and the Buy Button get it from
  the tag by themselves), or `metadata.boltclaw_click` beside `metadata.boltclaw_ref`. A
  subscription checkout's click is kept with the customer, so the first invoice is timed from it.
- **Any other billing system:** a `boltclaw_click` field in the same custom data that
  carries `boltclaw_ref`. `BoltClawReferral.attach(form)` adds both fields to a form.
- **The API:** `referral.click_id` on [`POST /v1/referrals/conversions`](https://boltclaw.ai/docs/api/referrals).

Without it nothing breaks; the timing rule simply has nothing to measure.

`data-clean-url` on the tag removes `via` and `bc_click` from the address bar once the tag
has read them, so a link somebody copies carries neither, and nor does anything that reads the
address after the tag. A router that writes back the address it was rendered with (Next.js's
App Router does, as it hydrates) is undone once the page has loaded. Analytics that read the
address before that still see both: give them the address without `via` and `bc_click` in
their own settings (`page_location` for Google Analytics, `beforeSend` for Vercel Analytics).

## The card check

With Stripe, add the `charge.succeeded` event to the webhook endpoint (the Bolt Claw
Stripe App receives it already). Bolt Claw turns the card's fingerprint into a keyed hash
for your workspace the moment it arrives and keeps only that, so it can say "this card
paid for 10 of one promoter's customers" without ever storing or showing the card. Without
the event every other rule still runs.

## The review queue

The Fraud tab, and [`GET /v1/referrals/programs/{id}/flags`](https://boltclaw.ai/docs/api/referrals), list
what waits for a decision, filtered by rule, promoter, score and age. Each flag shows the
score and every reason, a timeline of the customer's click, signup, sale and refunds with
the seconds between them, and the promoter's history in the program.

- **Dismiss**: not fraud. What it held continues to a payout.
- **Confirm**: fraud. What it held becomes `fraud` and is never paid; ban the promoter
  only if you also ask for it.
- **Hold this promoter's payouts**: a stop while you look. Nothing is taken away.

Dismiss and confirm work on up to 200 flags at once with one reason, each recorded on its
own. An AI assistant must always ask a person before it dismisses or confirms a flag, or
changes the settings.

**Promoters never see why.** Their side says "Under review" and nothing else, so nobody
learns which rule to dodge.

## Alerts

`referral.risk.alert` is sent to your [webhooks](https://boltclaw.ai/docs/webhooks) when a promoter first
needs a look or a code leak starts, at most once per promoter a day. Switch on the alert
email on the Fraud settings to have the program's managers emailed too.

## Protecting a promoter's payouts

Changing where a promoter is paid needs a sign-in within the last 15 minutes (and the
second step, if their account has one). After any change their payouts wait 48 hours, the
payout preview says "Payout details changed 3 hours ago", and their account's address gets
an email with a link that locks their payouts without signing in, in case it was not them.
Your own users' details, set through the in-app panel, raise
`referral.referrer.payout_details_changed` instead, so you can tell them.

## Applications

Ask up to three questions on the program's join page with `application_questions`. Each
application shows the answers, the promoter's website, how old their account is, and two
counts: near-identical applications sent to other programs this week ("Looks templated"),
and applications from one network and device in a day. Never which programs, which
accounts or an address.

## What is never stored

No visitor's IP address, no card fingerprint and no customer email is stored in the clear
or shown to anyone, hashed or not. Addresses are looked up against published lists of
cloud and hosting networks (X4BNet, Amazon Web Services, Google Cloud and a fixed set of
hosting networks), imported into our own database once a week; no visitor is ever sent to
a third party, and no third-party script runs on a referral link.

<!-- https://boltclaw.ai/docs/referral-fraud as markdown. Generated from openapi.yaml; API version 1.22.0. -->
<!-- Every /docs page answers as markdown: add .md to its path, or send Accept: text/markdown. -->
