> ## Documentation Index
> Fetch the complete documentation index at: https://usefoil.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# KYC fraud reduction

> Reduce KYC fraud before your vendor runs. Use Foil to catch synthetic identities, identity farming, and anti-detect browsers at the verification step.

<Info>
  Foil sits alongside your KYC vendor, not in place of it. The vendor checks *whether the identity is valid*; Foil checks *whether a real human at a real device is operating the form*. Stacking the two catches fraud that passes each layer individually - especially synthetic-identity farms, anti-detect browsers, and repeat submissions from one device under many identities.
</Info>

## The threat

KYC-gated flows (fintech onboarding, crypto exchanges, gambling, lending, payouts, any "high-limit" account upgrade) attract some of the most well-funded browser automation on the public internet. The rewards per account are large enough to justify real investment: stolen identity kits bought on dark markets, anti-detect browsers (\$100–\$500/month retail), residential proxy pools, and increasingly LLM-driven agents that can fill forms and respond to vendor prompts autonomously.

Four abuse patterns dominate:

* **Synthetic identities.** Fabricated or Frankenstein PII (real SSN, fake name; real name, fake DOB) submitted through automated browsers. Individual submissions may pass the KYC vendor's checks because the underlying data points look plausible; the tell is the *browser operating the form*, which Foil sees.
* **Identity farming.** Operator runs dozens or hundreds of accounts through KYC on one device using different PII each time, then resells the verified accounts. Every individual submission looks fine; the visitor fingerprint betrays the pattern.
* **Anti-detect browser fraud.** Multilogin, GoLogin, Kameleo, and similar tools specifically exist to defeat device fingerprinting - the KYC fraud market is their primary customer base. These environments push Foil's overall `risk_score` up even when the behavioral profile looks human, and the stored session's `runtime_integrity` surface explicitly flags tampering and virtualization.
* **Liveness / video bypass.** Virtual cameras, deepfakes, and face-swap tooling target the selfie/liveness step. This is primarily the KYC vendor's problem, but the *browser environment* used to run the bypass often trips Foil's virtualization and anti-tamper signals.

Against all four, Foil produces signal before the KYC vendor sees the request - cheap, high-precision, and useful in two ways: as a pre-check that stops obvious fraud before paying the vendor per-verification fee, and as an input to a layered decision for everything the vendor returns as "approved but we're not sure."

## The flow

<Steps>
  <Step title="Start Foil when the KYC flow begins">
    This might be the "Upgrade your account" button or the start of a dedicated onboarding journey. Collection needs time - KYC is one of the rare surfaces where you can afford to wait for fingerprint readiness.
  </Step>

  <Step title="Gate form submission on waitForFingerprint()">
    Unlike a login page, KYC users expect friction. It's fine to disable the final submit button until Foil's fingerprint has frozen - this guarantees you'll get a stable `visitor_fingerprint.id` for cross-session correlation.
  </Step>

  <Step title="Call getSession() at submit">
    Handoff travels with the KYC payload.
  </Step>

  <Step title="Pre-check the Foil verdict and risk score">
    Hard-block on `bot`. For `human` verdicts, apply a stricter `risk_score` threshold than you would on a lower-friction surface - a borderline score on a regulated flow usually warrants manual review.
  </Step>

  <Step title="Check the fingerprint's history">
    `GET /v1/fingerprints/:visitorId` - has this device run KYC before under a different identity?
  </Step>

  <Step title="Forward to the KYC vendor with Foil metadata attached">
    So the vendor's own decision engine can incorporate the signal on their side.
  </Step>

  <Step title="Monitor post-KYC sessions">
    A verified account that starts behaving like automation later is a strong account-takeover or farming signal. Keep Foil on sensitive in-app actions.
  </Step>
</Steps>

## Client integration

KYC is the one surface where `waitForFingerprint()` should block submission. False positives from slow fingerprinting are cheap here (the user will wait), and false negatives from submitting before fingerprint freeze are expensive (you lose the visitor ID that catches identity farming).

```html theme={"dark"}
<script type="module">
  const foilPromise = import("https://cdn.usefoil.com/t.js").then(
    (Foil) =>
      Foil.start({
        publishableKey: "pk_live_your_publishable_key",
      }),
  );

  const submitButton = document.querySelector("#kyc-submit");
  submitButton.disabled = true;

  foilPromise.then(async (foil) => {
    foil.onError((error) => {
      console.error("Foil error", error.code, error.message);
    });

    await foil.waitForFingerprint();
    submitButton.disabled = false;
  });

  async function submitKyc(formData) {
    const foil = await foilPromise;
    const { sessionId, sealedToken } = await foil.getSession();

    return fetch("/api/kyc/submit", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        ...formData,
        foil: { sessionId, sealedToken },
      }),
    });
  }
</script>
```

## Server verification

The pattern below uses a generic `kycVendor.verify(...)` placeholder. Substitute Persona, Onfido, Veriff, Sumsub, Socure, Alloy, or your internal equivalent.

The `KYC_RISK_SCORE_THRESHOLD` below is set at `30` as a starting point - well below Foil's default bot verdict threshold (\~`70`) but strict enough that genuinely suspicious sessions don't auto-approve on a regulated flow. `risk_score` is an integer from `0` to `100`; tune the threshold to your traffic.

<CodeGroup>
  ```javascript Node.js theme={"dark"}
  const { Foil, safeVerifyFoilToken } = require("@abxy/foil-server");
  const client = new Foil({ secretKey: process.env.FOIL_SECRET_KEY });

  const KYC_RISK_SCORE_THRESHOLD = 30;

  app.post("/api/kyc/submit", async (req, res) => {
    const tr = safeVerifyFoilToken(
      req.body.foil.sealedToken,
      process.env.FOIL_SECRET_KEY,
    );

    // 1. Fail closed - no valid token, no KYC.
    if (!tr.ok) {
      return res.status(403).json({ status: "rejected", reason: "verification_failed" });
    }

    const { decision, attribution, visitor_fingerprint } = tr.data;

    // 2. Hard block obvious automation before paying the KYC vendor.
    if (decision.verdict === "bot") {
      await logKycRejection({ reason: "bot", decision, kycPayload: req.body });
      return res.status(403).json({ status: "rejected", reason: "automation_detected" });
    }

    // 3. Human verdict but elevated score - KYC is a stricter surface than
    //    signup or checkout. A borderline score warrants manual review even
    //    when the top-level verdict is "human".
    if (decision.risk_score >= KYC_RISK_SCORE_THRESHOLD) {
      await flagForManualReview({ reason: "elevated_risk_score", decision });
      return res.json({ status: "manual_review" });
    }

    // 4. Identity farming - has this device already completed KYC?
    if (visitor_fingerprint?.id) {
      const priorClaims = await findPriorKycByVisitor(visitor_fingerprint.id);
      if (priorClaims.length > 0 && priorClaims.some((c) => c.userId !== req.session.userId)) {
        await flagForManualReview({ reason: "identity_farming", priorClaims });
        return res.json({ status: "manual_review" });
      }
    }

    // 5. Forward to the KYC vendor with Foil metadata attached.
    const vendorResult = await kycVendor.verify({
      user: req.body.user,
      documents: req.body.documents,
      metadata: {
        foil_session_id: req.body.foil.sessionId,
        foil_verdict: decision.verdict,
        foil_risk_score: decision.risk_score,
        foil_visitor_id: visitor_fingerprint?.id ?? null,
      },
    });

    // 6. Layered decision - inconclusive Foil + vendor "approved" = manual review.
    if (decision.verdict === "inconclusive" && vendorResult.status === "approved") {
      await flagForManualReview({ reason: "foil_inconclusive_despite_vendor_approval" });
      return res.json({ status: "manual_review" });
    }

    await recordKycResult({
      userId: req.session.userId,
      visitorId: visitor_fingerprint?.id ?? null,
      foilVerdict: decision.verdict,
      foilRiskScore: decision.risk_score,
      vendorResult,
    });

    res.json({ status: vendorResult.status, verified: vendorResult.status === "approved" });
  });
  ```

  ```python Python theme={"dark"}
  from foil_server import Foil, safe_verify_foil_token
  import os

  client = Foil(secret_key=os.environ["FOIL_SECRET_KEY"])
  KYC_RISK_SCORE_THRESHOLD = 30

  @app.post("/api/kyc/submit")
  def kyc_submit(request):
      tr = safe_verify_foil_token(
          request.json["foil"]["sealedToken"],
          os.environ["FOIL_SECRET_KEY"],
      )

      if not tr.ok:
          return {"status": "rejected", "reason": "verification_failed"}, 403

      decision = tr.data.decision
      visitor = tr.data.visitor_fingerprint

      if decision.verdict == "bot":
          return {"status": "rejected", "reason": "automation_detected"}, 403

      if decision.risk_score >= KYC_RISK_SCORE_THRESHOLD:
          flag_for_manual_review(reason="elevated_risk_score", decision=decision)
          return {"status": "manual_review"}

      if visitor:
          prior = find_prior_kyc_by_visitor(visitor["id"])
          if any(c["user_id"] != request.user.id for c in prior):
              flag_for_manual_review(reason="identity_farming", prior=prior)
              return {"status": "manual_review"}

      vendor = kyc_vendor.verify(
          user=request.json["user"],
          documents=request.json["documents"],
          metadata={
              "foil_session_id": request.json["foil"]["sessionId"],
              "foil_verdict": decision.verdict,
              "foil_risk_score": decision.risk_score,
              "foil_visitor_id": visitor["id"] if visitor else None,
          },
      )

      if decision.verdict == "inconclusive" and vendor.status == "approved":
          flag_for_manual_review(reason="foil_inconclusive_despite_vendor_approval")
          return {"status": "manual_review"}

      record_kyc_result(
          user_id=request.user.id,
          visitor_id=visitor["id"] if visitor else None,
          foil_verdict=decision.verdict,
          foil_risk_score=decision.risk_score,
          vendor_result=vendor,
      )
      return {"status": vendor.status, "verified": vendor.status == "approved"}
  ```

  ```go Go theme={"dark"}
  import foil "github.com/abxy-labs/foil-server-go"

  const kycRiskScoreThreshold = 30

  func kycSubmitHandler(w http.ResponseWriter, r *http.Request) {
      var body KycSubmitBody
      json.NewDecoder(r.Body).Decode(&body)

      tr := foil.SafeVerifyFoilToken(
          body.Foil.SealedToken,
          os.Getenv("FOIL_SECRET_KEY"),
      )
      if !tr.OK {
          writeJSON(w, 403, map[string]string{"status": "rejected", "reason": "verification_failed"})
          return
      }

      d := tr.Data.Decision

      if d.Verdict == "bot" {
          writeJSON(w, 403, map[string]string{"status": "rejected", "reason": "automation_detected"})
          return
      }

      if d.RiskScore >= kycRiskScoreThreshold {
          flagForManualReview("elevated_risk_score", tr.Data)
          writeJSON(w, 200, map[string]string{"status": "manual_review"})
          return
      }

      if v := tr.Data.VisitorFingerprint; v != nil {
          prior := findPriorKycByVisitor(v.ID)
          if hasOtherUsers(prior, currentUserID(r)) {
              flagForManualReview("identity_farming", tr.Data)
              writeJSON(w, 200, map[string]string{"status": "manual_review"})
              return
          }
      }

      vendor := kycVendor.Verify(body, tr.Data)
      if d.Verdict == "inconclusive" && vendor.Status == "approved" {
          flagForManualReview("foil_inconclusive_despite_vendor_approval", tr.Data)
          writeJSON(w, 200, map[string]string{"status": "manual_review"})
          return
      }

      recordKycResult(currentUserID(r), tr.Data, vendor)
      writeJSON(w, 200, map[string]any{"status": vendor.Status})
  }
  ```

  ```ruby Ruby theme={"dark"}
  require "foil/server"

  KYC_RISK_SCORE_THRESHOLD = 30

  post "/api/kyc/submit" do
    body = JSON.parse(request.body.read)
    tr = Foil::Server::SealedToken.safe_verify_foil_token(
      body.dig("foil", "sealedToken"),
    )
    halt 403, { status: "rejected", reason: "verification_failed" }.to_json unless tr[:ok]

    decision = tr[:data][:decision]
    visitor  = tr[:data][:visitor_fingerprint]

    if decision[:verdict] == "bot"
      halt 403, { status: "rejected", reason: "automation_detected" }.to_json
    end

    if decision[:risk_score] >= KYC_RISK_SCORE_THRESHOLD
      flag_for_manual_review(reason: "elevated_risk_score")
      halt 200, { status: "manual_review" }.to_json
    end

    if visitor
      prior = find_prior_kyc_by_visitor(visitor[:id])
      if prior.any? { |c| c[:user_id] != current_user.id }
        flag_for_manual_review(reason: "identity_farming", prior: prior)
        halt 200, { status: "manual_review" }.to_json
      end
    end

    vendor = kyc_vendor.verify(body, foil: tr[:data])
    if decision[:verdict] == "inconclusive" && vendor.status == "approved"
      flag_for_manual_review(reason: "foil_inconclusive_despite_vendor_approval")
      halt 200, { status: "manual_review" }.to_json
    end

    record_kyc_result(current_user, tr[:data], vendor)
    { status: vendor.status }.to_json
  end
  ```

  ```php PHP theme={"dark"}
  use Foil\Server\SealedToken;

  const KYC_RISK_SCORE_THRESHOLD = 30;

  $body = json_decode(file_get_contents("php://input"), true);
  $tr = SealedToken::safeVerify(
      $body["foil"]["sealedToken"],
      getenv("FOIL_SECRET_KEY"),
  );

  if (!$tr->ok) {
      http_response_code(403);
      echo json_encode(["status" => "rejected", "reason" => "verification_failed"]);
      exit;
  }

  $d = $tr->data->decision;

  if ($d["verdict"] === "bot") {
      http_response_code(403);
      echo json_encode(["status" => "rejected", "reason" => "automation_detected"]);
      exit;
  }

  if ($d["risk_score"] >= KYC_RISK_SCORE_THRESHOLD) {
      flagForManualReview("elevated_risk_score", $tr->data);
      echo json_encode(["status" => "manual_review"]);
      exit;
  }

  $visitorId = $tr->data->visitor_fingerprint["id"] ?? null;
  if ($visitorId) {
      $prior = findPriorKycByVisitor($visitorId);
      foreach ($prior as $claim) {
          if ($claim["user_id"] !== currentUserId()) {
              flagForManualReview("identity_farming", $tr->data);
              echo json_encode(["status" => "manual_review"]);
              exit;
          }
      }
  }

  $vendor = kycVendor_verify($body, $tr->data);
  if ($d["verdict"] === "inconclusive" && $vendor->status === "approved") {
      flagForManualReview("foil_inconclusive_despite_vendor_approval", $tr->data);
      echo json_encode(["status" => "manual_review"]);
      exit;
  }

  recordKycResult(currentUserId(), $tr->data, $vendor);
  echo json_encode(["status" => $vendor->status]);
  ```

  ```bash cURL theme={"dark"}
  # Session readback - useful when reviewing a queued KYC submission later
  curl https://api.usefoil.com/v1/sessions/sid_... \
    -H "Authorization: Bearer sk_live_..."

  # Pull visitor fingerprint history for identity-farming investigation
  curl https://api.usefoil.com/v1/fingerprints/vis_... \
    -H "Authorization: Bearer sk_live_..."
  ```
</CodeGroup>

## Layered decision matrix

The decision you hand back to your own business logic (and record on the KYC row) should be the *product* of Foil and the vendor verdict, not either one alone. `risk_score` is an integer `0`–`100`; below the `bot` verdict threshold of \~`70` there's still a lot of useful gradient, and KYC is the surface where you should care about it.

| Foil verdict   | `risk_score` | Vendor result | Recommended action                                                            |
| -------------- | ------------ | ------------- | ----------------------------------------------------------------------------- |
| `human`        | `< 30`       | `approved`    | Approve.                                                                      |
| `human`        | `< 30`       | `declined`    | Decline (vendor-driven).                                                      |
| `human`        | `30 – 69`    | `approved`    | Manual review. Borderline scores on a regulated flow should not auto-approve. |
| `human`        | `30 – 69`    | `declined`    | Decline.                                                                      |
| `inconclusive` | any          | `approved`    | Manual review. Don't auto-approve borderline sessions on regulated flows.     |
| `inconclusive` | any          | `declined`    | Decline.                                                                      |
| `bot`          | any          | any           | Reject before the vendor. Log and rate-limit by IP and visitor fingerprint.   |

Two principles worth holding:

* **Never auto-approve on vendor-only signal for a KYC-gated flow.** If Foil's `risk_score` is elevated or the verdict is `inconclusive`, the vendor saying "approved" is not enough to promote an account to its full permission set.
* **Use a stricter `risk_score` threshold than you would on other surfaces.** Foil's default `bot`/`human` cutoff is tuned for broad use across signup, login, and content surfaces; KYC's false-negative cost justifies treating anything above roughly `30` as worth a human look.

## Runtime integrity: deeper environment signal

When the sealed-token verdict isn't enough - high-limit approvals, compliance-heavy jurisdictions, repeat-review queues - fetch the stored session and inspect `runtime_integrity`. These are discrete named flags computed server-side from the full observation stream, and they let you write precise rules instead of thresholding a single aggregated score.

```javascript Node.js theme={"dark"}
const session = await client.sessions.get(sessionId);
const ri = session.runtime_integrity;
// {
//   tampering_detected: boolean,
//   developer_tools_detected: boolean,
//   emulation_suspected: boolean,
//   virtualization_suspected: boolean,
//   privacy_hardening_suspected: boolean,
//   identity_spoofing_suspected: boolean,
//   replay_suspected: boolean,
//   outdated_environment: boolean,
//   verified_bot_identity: boolean,
// }
```

| Flag                          | What it means on a KYC submission                                                                                                                                                                                                                                                                                                                                          |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tampering_detected`          | Explicit environment tampering (patched navigator properties, stealth-plugin artifacts, injected globals). On a KYC flow this is a strong manual-review signal regardless of the sealed-token verdict.                                                                                                                                                                     |
| `emulation_suspected`         | Device emulation - typically a mobile emulator on a desktop. Almost always fraud when the vendor expects a real phone for document capture and liveness.                                                                                                                                                                                                                   |
| `virtualization_suspected`    | Submission is coming from a VM. Legitimate in some corporate setups (Citrix, VDI) but overrepresented in fraud operations. Weight it; don't auto-reject.                                                                                                                                                                                                                   |
| `developer_tools_detected`    | DevTools open during submission. Common on legitimate support calls and among advanced users - useful context, not dispositive.                                                                                                                                                                                                                                            |
| `privacy_hardening_suspected` | Hardened privacy browser (Firefox `resistFingerprinting`, Brave shields, Tor). Usually a real user; primarily helpful as an explanation for a missing `visitor_fingerprint.id`.                                                                                                                                                                                            |
| `identity_spoofing_suspected` | The device's self-reported identity disagrees with itself - UA vs Client-Hints, locale vs keyboard, IP geo vs timezone, plugin set vs Chrome runtime. Common on KYC fraud rigs that swap UA strings without re-aligning the rest of the surface.                                                                                                                           |
| `replay_suspected`            | The session's snapshot, batch sequence, or anti-replay nonce looks reused or out of order - typically a captured-then-replayed submission rather than a live browser. Manual review when paired with any other risk signal.                                                                                                                                                |
| `outdated_environment`        | The browser claims a modern version but its capability surface (CSS @supports, Chrome API markers, TLS cipher suites, Sec-CH-UA-Full-Version-List) looks legacy. Common on automation kits that pin a stale Chromium build under a freshly-rotated UA string.                                                                                                              |
| `verified_bot_identity`       | A *positive* signal: the request carried a valid Web Bot Auth signature. When `true`, the session is a self-identified, cryptographically-attested bot - usually an explanation for an automated verdict rather than a fraud signal. KYC flows generally still don't accept these submissions, but the operational response is "block politely" rather than "investigate". |

`tampering_detected`, `emulation_suspected`, and `identity_spoofing_suspected` deserve a manual-review route even when the sealed-token verdict is `human` and the `risk_score` is under threshold. They're the signals most likely to be *silent* on the aggregated score when an attacker is specifically tuning against it.

`runtime_integrity` is only available via `GET /v1/sessions/:sessionId` - it's not on the sealed token. Two integration shapes work well:

* **Session readback on every KYC submission.** One extra API call per verification; negligible next to the cost of the KYC vendor itself.
* **Session readback only on borderline cases.** Fast path on clean `human` + low-score submissions; fetch the stored session for everything else, including the `manual_review` paths in the code above.

## Identity farming: cross-session correlation

The visitor fingerprint is the axis that catches farms. One device running KYC under five identities over two weeks looks human on every individual submission - it *is* human, just a human engaged in fraud. The tell is in `GET /v1/fingerprints/:visitorId`.

```javascript Node.js theme={"dark"}
async function findSuspiciousKycHistory(visitorId) {
  const fingerprint = await client.fingerprints.get(visitorId);

  // Count distinct accounts that have run KYC from this device
  const priorKyc = await db.query(
    "SELECT DISTINCT user_id, submitted_at FROM kyc_submissions WHERE visitor_id = $1 ORDER BY submitted_at DESC",
    [visitorId],
  );

  return {
    seenCount: fingerprint.lifecycle.seen_count,
    firstSeenAt: fingerprint.lifecycle.first_seen_at,
    distinctKycAccounts: priorKyc.rows.length,
    priorBotSessions: fingerprint.activity.sessions.filter(
      (s) => s.decision.verdict === "bot",
    ).length,
  };
}
```

Practical thresholds for flagging:

* **2+ distinct accounts from the same visitor fingerprint** completing KYC → manual review. Real households exist, but KYC-gated flows rarely involve two adults on the same device.
* **Any prior `verdict: "bot"` on that fingerprint in the last 90 days** → manual review. The device has failed Foil before.
* **`lifecycle.seen_count > 50` with first-seen under 24 hours** → auto-reject. That's a headless browser being spun up repeatedly on one machine.

See [Promo & trial abuse](/use-cases/promo-abuse) for a more general treatment of the cross-session correlation pattern; the KYC version is essentially that page with tighter thresholds and a manual-review default.

## Attaching Foil signal to the vendor

Most KYC vendors expose a metadata or `custom_fields` parameter on their verification API. Putting Foil's session ID and verdict there pays off in two ways:

* **Vendor-side rules.** Modern KYC platforms (Alloy, Sardine, Socure, Sumsub rules engine) let you write decisioning on custom fields. "Decline if `foil_risk_score >= 30` AND document score below 0.9" is a one-line rule that would otherwise require integrating Foil into the vendor's system separately.
* **Post-hoc reconciliation.** When a chargeback, SAR, or compliance audit surfaces a KYC approval months later, the Foil session ID stamped on the vendor record lets you pull `GET /v1/sessions/:sessionId` and recover the full device evidence - including `runtime_integrity` and `signals_fired` - from the time of submission.

Which fields to send:

```json theme={"dark"}
{
  "foil_session_id": "sid_...",
  "foil_verdict": "human",
  "foil_risk_score": 12,
  "foil_visitor_id": "vis_...",
  "foil_attribution_category": "human"
}
```

`foil_attribution_category` pulls from `attribution.bot.facets.category.value` when present; on human verdicts it's generally `null`, so you may want to store `"human"` for consistency.

## Post-KYC monitoring

Getting a `human` verdict at KYC submission is necessary but not sufficient. Account takeover happens later - the KYC is clean, but six months on, someone logs in from a new device, hardens their settings, and starts draining the account or withdrawing funds. Keep Foil on:

* Every login (see [Login & credential stuffing](/use-cases/login)).
* Payout / withdrawal / transfer initiation.
* Limit-increase requests (the typical next step after compromise).
* Any sensitive setting change - email, password, 2FA, beneficiary.

A KYC-verified account that suddenly starts producing `bot` verdicts or an elevated `risk_score` on withdrawals is a very strong ATO signal, and the fact that the account is KYC-verified makes the response *more* important, not less - the attacker has done the work to look legitimate precisely to unlock high-limit actions.

<Warning>
  KYC is a regulated flow in most jurisdictions. How long you retain Foil verdicts and visitor fingerprints on KYC records is a compliance question, not just an engineering one. Confirm retention requirements with your privacy/legal team before you ship. See [Privacy & data](/privacy-and-data) for Foil's side of the data picture.
</Warning>

## What's next

<CardGroup cols={2}>
  <Card title="Signup protection" icon="user-plus" href="/use-cases/signup">
    Stop automated account creation before KYC even starts.
  </Card>

  <Card title="Promo & trial abuse" icon="ticket-percent" href="/use-cases/promo-abuse">
    The same cross-session correlation pattern, at lower stakes.
  </Card>

  <Card title="Verdicts & scoring" icon="gauge" href="/verdicts-and-scoring">
    Deep-dive on `verdict`, `risk_score`, and where thresholds come from.
  </Card>

  <Card title="Privacy & data" icon="lock" href="/privacy-and-data">
    What Foil stores and for how long.
  </Card>
</CardGroup>
