# Stop OTP abuse with block data and disposable numbers

Which numbers soak up one-time codes and free trials, the four open signals that catch them, and a signup policy that does not punish real people.

Date: 2026-09-05

A one-time code is only as good as the number it goes to. If anyone can read the code, the number is not proof of a person, and every product that gates a free trial, a coupon or a second account behind SMS has met the same numbers doing it: rented virtual numbers, numbers published on websites that show incoming texts to the world, and the digit patterns people type when they do not want to be reached. This guide walks through the four open signals Spaw reads for every lookup, what each one proves, and how to gate a signup with them without blocking the people you want.

## Where the numbers come from

Most abuse does not come from a handset. It comes from a number that was never meant to be one person's phone:

- **Virtual numbers.** Wholesalers such as Bandwidth, Onvoy and Telnyx in the United States, Iristel and Fibernetics in Canada, and a handful of European carriers hold blocks whose numbers are resold by the thousand to apps, call centres and SMS-receiving services. A number in one of those blocks can be rented for minutes.
- **Numbers published on SMS-receiving websites.** These sites show every incoming message on a public page. Anyone can pick a number, use it for a signup, and read the code without owning anything.
- **Fictional ranges.** The North American 555-01XX block and Ofcom's drama ranges exist so that films and television never dial a real person (https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbers-for-drama, accessed 5 September 2026). Nobody will ever answer them, which makes them a favourite for people filling in a required field.
- **Digit patterns.** 555-5555 and 123-4567 are what people type when the form insists on a number and they do not intend to give one.

None of these is fraud in itself. All of them mean the code will not reach a person's own phone.

## The four signals and what each proves

Spaw reads all four from data it holds locally, so every signal is in the same answer at the same single credit.

| Signal | Field | Where it comes from |
| --- | --- | --- |
| Virtual-number block | `is_virtual: true`, `line_type: "voip"` with `line_type_source: "block_allocation"` | The regulator's allocation table names the block holder; a wholesaler's name settles it. |
| Published on an SMS-receiving website | `is_disposable: true`, `disposable_seen_at` | An open list of numbers scraped from those sites, synced weekly, with the date each number was last seen. |
| Fictional range | `is_fictional: true` | The published fiction and drama ranges, bundled with the service. |
| Digit pattern | `digit_pattern` in `risk_signals` | The last seven digits repeat one digit or run straight up or down. |

The disposable list deserves one honest note. The open list Spaw syncs (https://github.com/iP1SMS/disposable-phone-numbers, accessed 5 September 2026) records when each number was last seen on a site, and most entries are old: of the 126,076 numbers on the copy synced on 5 September 2026, 109,509 were last seen more than a year earlier. A number a site showed two years ago may have been returned to the carrier and reassigned to a real subscriber since. That is why a listing within the last year scores 70 points and an older one 40, and why the date is in the answer for you to weigh yourself.

The virtual-block signal has its own caveat. A wholesaler's block backs plenty of legitimate use: a business phone system, a customer's second line, a number from an app that people use as their everyday number. `is_virtual` means the number was allocated to a wholesaler, not that the person behind it is up to something.

## Read the score, then decide

The risk score is a sum of published weights over these flags, capped at 100, and `risk_signals` lists every term so you can recompute it. The weights that matter here: fictional 80, disposable 70, disposable_stale 40, virtual 40, voip 30, digit_pattern 30. A number in a wholesaler block that also appears on an SMS-receiving site this year scores 110 before the cap, so 100 and `high`. A number in a wholesaler block and nothing else scores 40 and `medium`. A plain mobile in a carrier's block scores 0.

Spaw does not call any of this fraud. The score measures how unlikely a number is to reach one person's own handset, which is exactly the question a one-time code asks.

## A policy that does not punish real people

Block only on the signals that prove the code cannot land, and step up instead of blocking on the signals that merely lower the odds:

1. **Reject invalid numbers outright.** `valid: false` with any reason, including `unassigned_block`, costs nothing and never reaches anyone. Show the reason in plain words: a missing country, a number that is too short, a block nobody holds.
2. **Reject fictional numbers and fresh disposable listings.** `is_fictional` and a `disposable_seen_at` within the last year are the two signals that mean "anyone or no one can read this code". Ask for another number.
3. **Step up on medium.** A wholesaler block, a stale listing or a digit pattern is a reason to send the code and also require something else, such as an email confirmation or a delay before the trial unlocks, not a reason to refuse the person. Wholesaler blocks are full of real customers.
4. **Never block on `fixed_line_or_mobile`.** In North America that is the plan's honest answer for most real mobiles. Blocking it blocks your customers.
5. **Keep the answer.** Store `e164`, the flags and `risk_score` next to the account so a later review can see why a signup was allowed.

Run the policy from the browser and the server both. The form helper marks the field as the person types, with `data-spaw-phone`, and the server checks the same number again on submit, because a page can be edited. Both calls answer from the same tables, and a repeat of the same number within seven days is free.

## Test it before you ship it

The seven test numbers answer fixed results and never cost a credit: +1 202 555 0102 is a virtual number, 0103 is a fresh disposable listing, 0105 is fictional, 0104 sits in an unassigned block and 0100 is a clean mobile. Wire them into your integration tests so the policy above is exercised on every deploy, without depending on what the open lists say that week.

## What to do next

- Read the [phone intelligence guide](/docs/phone-intelligence) for every field and the full weight table.
- Check the request and response on the [POST /api/v1/phone reference](/docs/api/validate-phone).
- Validate from a page with the [publishable-key endpoint](/docs/api/validate-phone-public) and the form helper.
- Understand the plan behind the `fixed_line_or_mobile` answer in [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline).

Reference: https://spaw.co/guides/stop-otp-abuse-with-block-data-and-disposable-numbers
