Repository navigation
Check signup emails with the UserCheck API directly instead of through RapidAPI #656
Description
Summary
Signups and email changes are checked by check_email_domain() (users/utils.py), which calls MailCheck through RapidAPI. UserCheck, the same service under its current name, has a direct API that returns more signals for spotting fake or throwaway addresses. Moving to it would let us block more bad signups and drop the RapidAPI middleman.
Why
- Fake accounts still get through, and we are spending waaay time than we should attempting to contact users to keep the API healthy.
- UserCheck's
GET /email/{email}endpoint returns signals we don't use today:disposable: the address or domain is a throwaway service.relay_domain: a forwarding service that hides the real mailbox. This would cover most of our hand-kept blocklist (duck.com,simplelogin.com,passmail.net).spam: the domain is linked to abuse.mx: the domain can actually receive mail.free_subdomain: a subdomain of a free or dynamic DNS service.domain_age_in_days: brand-new domains are a common sign of abuse.did_you_mean: suggests a fix for typos likegmial.com, which helps real users finish signing up.normalized_email: collapses+tagsand provider aliases, so we can catch the same mailbox signing up more than once.
Cost: can we stay on the free plan?
From the Credits and Rate limits docs:
| Plan | Credits per month | Requests per second | Price |
|---|---|---|---|
| Free | 1,000 | 1 | $0 |
| Pro 200k | 200,000 | 5 | $50/mo |
Each email or domain lookup costs 1 credit. Only successful requests use credits, and unused credits don't carry over. When credits run out, the API returns 429 until the billing cycle resets.
What uses credits for us:
- Every signup attempt whose domain isn't on the allowlist (
gmail.com,yahoo.com,proton.me) or the blocklist. That includes attempts that fail later, such as bots that get past hCaptcha. - Every email change on a profile.
- Development and staging, which share the same quota.
Current signup volume
Monthly signups from a recent production snapshot (data through 2026-09-25). Only addresses outside the allowlist cost a credit:
| Month | New users | Need an API lookup |
|---|---|---|
| 2025 (Jan–Nov) | 27–53/mo | 10–23/mo |
| Dec 2025 | 99 | 30 |
| Jan 2026 | 169 | 55 |
| Feb 2026 | 149 | 45 |
| Mar 2026 | 163 | 60 |
| Apr 2026 | 180 | 51 |
| May 2026 | 220 | 74 |
| Jun 2026 | 221 | 70 |
| Jul 2026 | 283 | 86 |
| Aug 2026 | 583 | 103 |
| Sep 2026 (partial) | 397 | 111 (~130 projected) |
- Signups have taken off: about 10 times more a month than in 2025.
- Most new users don't cost a credit. About 70% sign up with an allowlisted domain.
- Successful signups use roughly 10–13% of the free plan's 1,000 credits. That leaves about 870 credits a month for everything else.
- This slightly undercounts lookups. The counts use each user's current email, so anyone who signed up on a non-allowlisted domain and later switched to Gmail is missed.
The remaining unknown is how many lookups come from failed signup attempts and email changes, which use credits but leave no new user behind. Before deciding, we should:
- Check the RapidAPI usage dashboard for actual monthly calls, or count
users.formswarnings in the logs. - Confirm the peak month stays well under 1,000, leaving room for further growth and spikes.
If we're close to the limit, we could cache results per domain (for example 30 days in Redis), since most lookups repeat the same domains. We could also switch to the /domain/{domain} endpoint for domains we've already seen. The current usage is also available from the free GET /status endpoint.
Handling 429 responses
The free plan allows only 1 request per second, so a 429 can mean either:
- Rate limited: two lookups landed in the same second.
- Out of credits: the monthly quota is used up.
Today any error from the API makes check_email_domain() return None, and the user sees "Error creating account. Contact the site administrator." That means:
- Two people signing up in the same second would make one of them fail.
- If we run out of credits partway through a month, every signup from a non-allowlisted domain would be blocked until the reset.
Proposed handling:
- On
429, retry once after about a second, to cover the rate limit. - If it still fails, either:
- Fail open: let the signup through and fall back to the local blocklist, logging the miss.
- Fail closed: keep today's behavior.
- Either way, send a Pushover alert (
send_pushover()already exists) on repeated429s so we know.
Proposed changes
- Rewrite
check_email_domain()to callhttps://api.usercheck.com/email/{email}withAuthorization: Bearer <key>. URL-encode the address. - Replace
RAPID_API_KEYandRAPID_API_HOSTwithUSERCHECK_API_KEYin settings,.env.exampleandmetron.env.example. - Map the response in
validate_email_allowed():disposableorspam→ "Temporary email addresses are not allowed."relay_domain→ reject. This one is a policy decision, because some privacy-minded users rely on relays.mx is False→ "Email address is not valid."did_you_mean→ show it in the form error.
- Decide what to do with the hand-kept blocklist and allowlist. We might be able to replace the allowlist with
public_domain, but keeping the allowlist saves credits. - Handle
429: retry once, then fail open or closed, and send the Pushover alert. - Optional: cache results per domain to save credits.
- Update the tests in
tests/users/test_utils.pyandtests/users/test_email_change.py.
Open questions
- How many lookups a month come from failed signup attempts and email changes?
- Should relay addresses be blocked?
- Fail open or closed when we're out of credits?
Activity
jyggen commentedon Oct 16, 2026
Packages like email-validator and disposable-email-domains are worth considering as well. If not to fully remove the dependency on a 3rd-party, then at least as a first level of defense to reduce the quota we need to (potentially) pay for.
bpepple commentedon Oct 16, 2026
Pypi moved to UserCheck, so I spent a few hours looking into it this morning and I'm pretty impressed by what they offer. Now, our current number of new users per month should be fine with their free plan using some of the methods I mention above (and possibly your suggestions), but I'm going to reach out to them to see if they have any plans to offer something more feasible for a community project, since it $50/mo is about 1/2 of our server costs. 😲
Check signup emails with UserCheck and disposable-email-domains
Check signup emails with UserCheck directly and the disposable-email-…