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

# Stop SMS pumping

> Block fraudulent OTP requests that rack up SMS costs. Catch bad phone numbers, OTP velocity, and automated dialers before you pay for the message.

SMS pumping (also called artificially inflated traffic, or AIT) is a fraud pattern where attackers trigger OTP messages to phone numbers they control (often premium-rate or VOIP numbers) and collect the revenue your SMS provider pays out. You pay per message; they profit per delivery. A single unprotected OTP endpoint can generate thousands of dollars in SMS charges overnight.

The attack is simple to script and hard to notice until your SMS bill arrives. The fix is catching it before the message is sent: evaluate the phone number and the sending velocity against your `otp` policy, and only fire the SMS if tracenow clears it.

## Which event this protects

SMS pumping maps to the `otp` event. The Cloudflare Worker fires it on every POST to your OTP-send route, before your app calls your SMS provider. If the verdict is `deny`, the message is never sent and you pay nothing.

## How it works

When a request hits your OTP endpoint, the Worker:

1. Reads the phone number from the request body and the IP from the request.
2. Attaches the device fingerprint token.
3. Calls `/trace` with `event: otp`; phone enrichment and velocity checks run in parallel.
4. Evaluates your SMS pumping policy.
5. `deny` → the Worker returns a block response; your SMS provider is never called. `allow`/`challenge` → forwarded to your app.

### Signals used

| Signal                         | Why it matters                                                            |
| ------------------------------ | ------------------------------------------------------------------------- |
| `phone.is_voip`                | VOIP numbers are free to create in bulk and commonly used in AIT schemes  |
| `phone.is_burner`              | Disposable numbers; no real user needs a throwaway number for OTP         |
| `phone.valid`                  | Malformed or non-dialable numbers; nothing to send, so deny immediately   |
| `phone.carrier_country_risk`   | Carriers in high-risk countries are disproportionately involved in AIT    |
| `velocity.ip.trace_count_5m`   | More than 5 OTP requests from one IP in 5 minutes = a dialer              |
| `velocity.user.trace_count_5m` | A single account requesting OTPs faster than any human                    |
| `velocity.user.trace_count_1h` | Sustained OTP abuse over the hour                                         |
| `ip.is_tor`                    | Tor is used to rotate the apparent sending IP                             |
| `ip.is_hosting`                | Data-center IPs send OTP requests programmatically, not via real browsers |
| `ip.is_proxy`                  | Open proxies used to distribute the traffic and evade rate limits         |
| `device.is_headless`           | No real user is behind the request                                        |
| `device.automation_detected`   | Scripted browser driving OTP requests                                     |

See [Signals](/concepts/signals) for the full field reference.

<Note>
  **Fully covered at the edge.** SMS-pumping signals (phone quality, IP, velocity, device) are all
  available before sign-in, so the Cloudflare Worker covers this use case with no backend code.
  Add the optional [server-side call](/guides/server-side) only if you want per-user OTP-failure
  velocity tied to a real account.
</Note>

## Set it up

<Tip>
  **Fastest setup:** enable the pre-built [SMS pumping policy templates](/guides/policy-templates)
  in Monitor mode instead of building these rules by hand. The steps below show what each
  template contains, so you can tune them afterward.
</Tip>

<Steps>
  <Step title="Connect Cloudflare (if you haven't already)">
    In the dashboard under **Integration**, paste a Cloudflare API token with **Workers Scripts: Edit**
    and **Zone → DNS: Read** scopes. tracenow validates the token and lists your zones.
  </Step>

  <Step title="Add a route rule for your OTP endpoint">
    Under **Integration → Routes**, add a rule:

    * **Route pattern:** e.g. `yourdomain.com/api/auth/otp` or `yourdomain.com/send-otp`
    * **Method:** `POST`
    * **Event:** `otp`
    * **Mode:** start with `Monitor` to observe real traffic before blocking
  </Step>

  <Step title="Enable the SMS pumping policies">
    Under **Guard**, enable the pre-built [SMS pumping policy templates](/guides/policy-templates) for the `otp` event in **Monitor** mode. To customize or add your own rules, see [Rules](/guides/rules).
  </Step>

  <Step title="Switch to Enforce mode">
    After reviewing Monitor traces and confirming the rules catch the right traffic, edit the OTP route and change **Mode** to `Enforce`. Click **Deploy**.

    In Enforce mode, `deny` verdicts stop the request at the edge, so your SMS provider never receives the call. This is the critical step: the protection only prevents charges when Enforce mode is active.

    <Warning>
      Start with Monitor mode for at least 24–48 hours before switching to Enforce. An overly aggressive velocity rule could deny OTPs for legitimate users (e.g., users on shared corporate IPs). Review the trace feed first.
    </Warning>
  </Step>
</Steps>

## Rules

You don't build these by hand. The pre-built [SMS pumping policy templates](/guides/policy-templates) cover these patterns and enable in one click. See [Rules](/guides/rules) to customize them or add your own.

## What a caught attempt looks like

A pumping run produces many traces like this in the event feed:

```json theme={null}
{
  "trace_id": "tr_01hy...",
  "verdict": "deny",
  "event_type": "otp",
  "status": "attempted",
  "signals": {
    "phone": {
      "valid": true,
      "is_voip": true,
      "is_burner": false,
      "carrier_country_risk": "high"
    },
    "ip": {
      "is_tor": false,
      "is_hosting": true,
      "is_proxy": false,
      "geo": { "country": "Netherlands", "country_code": "NL" },
      "as": { "asn": "AS60781", "name": "LeaseWeb Netherlands B.V.", "type": "hosting" }
    },
    "device": {
      "is_headless": true,
      "automation_detected": true
    },
    "velocity": {
      "ip": { "trace_count_5m": 31, "trace_count_1h": 184 },
      "user": { "trace_count_5m": 4, "trace_count_1h": 22 }
    },
    "decision": {
      "rule_name": "Block VOIP and burner numbers",
      "logic": "OR",
      "matched_conditions": []
    }
  },
  "policy": {
    "name": "SMS pumping",
    "action": "deny",
    "rule_name": "Block VOIP and burner numbers"
  }
}
```

The `duration_ms` on OTP traces is typically low. Phone enrichment is fast, so by the time you'd call your SMS provider, the verdict is already back.

## Going further

The Cloudflare layer is the right tool for SMS pumping. OTP requests are pre-authentication, so there's no user ID to attach, and the edge evaluation is sufficient.

If your app verifies an OTP code (not just sends it), you can send a follow-up `/trace` call with `status: succeeded` or `status: failed` after the user enters the code. This seeds OTP failure velocity, which lets you catch accounts that request many codes but never enter a valid one: a signal of probing or credential enumeration.

<CodeGroup>
  ```typescript TypeScript theme={null}
  // After the user submits the OTP code for verification
  const result = await fetch("https://api.tracenow.io/trace", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TRACENOW_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      event: "otp",
      status: otpValid ? "succeeded" : "failed",
      ip: req.ip,
      phone: user.phone,
      device_token: req.headers["x-tn-device-token"],
      subject_user_id: user.id,
    }),
  });
  ```

  ```python Python theme={null}
  import httpx, os

  result = httpx.post(
    "https://api.tracenow.io/trace",
    headers={"Authorization": f"Bearer {os.environ['TRACENOW_SECRET_KEY']}"},
    json={
      "event": "otp",
      "status": "succeeded" if otp_valid else "failed",
      "ip": request.remote_addr,
      "phone": user.phone,
      "device_token": request.headers.get("x-tn-device-token"),
      "subject_user_id": str(user.id),
    },
  ).json()
  ```
</CodeGroup>

The `velocity.user.failed_count_1h` signal on the `otp` event then catches accounts burning through OTP codes, a pattern distinct from the sending-side abuse the Cloudflare layer catches.

See [server-side integration](/guides/server-side) for the full request and response reference.
