Skip to content

Check signup emails with the UserCheck API directly instead of through RapidAPI #656

Description

@bpepple

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 like gmial.com, which helps real users finish signing up.
    • normalized_email: collapses +tags and 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:

  1. Check the RapidAPI usage dashboard for actual monthly calls, or count users.forms warnings in the logs.
  2. 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 repeated 429s so we know.

Proposed changes

  • Rewrite check_email_domain() to call https://api.usercheck.com/email/{email} with Authorization: Bearer <key>. URL-encode the address.
  • Replace RAPID_API_KEY and RAPID_API_HOST with USERCHECK_API_KEY in settings, .env.example and metron.env.example.
  • Map the response in validate_email_allowed():
    • disposable or spam → "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.py and tests/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

  1. self-assigned this
    on Oct 16, 2026
  2. added theissue type on Oct 16, 2026
  3. jyggen commented on Oct 16, 2026

    @jyggen
    Member

    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.

  4. bpepple commented on Oct 16, 2026

    @bpepple
    MemberAuthor

    3. 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.

    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. 😲

  5. added a commit that references this issue on Oct 17, 2026
    3680fd5
  6. added a commit that references this issue on Oct 17, 2026
    edc6223
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions