DEV Community

IP99
IP99

Posted on

Cloud Phones Look Like Real Mobiles: Catch Farms at the IP Layer

Your signup funnel sees a "phone": WebView UA, touch events, maybe a real device fingerprint. The account farm on the other side is a rack of cloud phones — real or virtual handsets hosted in a datacenter, remoted into by operators who spin up hundreds of profiles a day.

Device-side checks often say "mobile." Network-type blocklists often say nothing useful: the egress sits in hosting space, or it sits behind whatever the vendor leased. What you need at registration, claim, or payout is an IP-layer signal that names the pattern, not a guess about the handset.

IP99 by ThreatHunter exposes that as cloud_phone on the free GET /v1/ip/{ip} response — one of ten typed signals — alongside evidence_state and a 0–100 score that measures freshness and strength of verifiable evidence, not "probability this person is an attacker."

What a cloud phone is (and is not)

Thing What it is What a naive rule sees
Real cellular user Handset on a mobile ASN (usage_type: MOB) Mobile UX + mobile network
Cloud phone farm Handset (or emulator) in a rack, remoted for bulk signup / farming Mobile UX; network often hosting or unrelated
Residential proxy Home broadband resold as an exit Residential ASN; no phone at all
Dial-up pool ISP lines that redial for a new address Residential space; churning egress

Official IP99 signal table: cloud_phone means a cloud phone (a phone device in a datacenter). It is a risk signal. Contrast mobile and hosting: those describe the network, not risk evidence — Google Public DNS carries hosting with score 0. Do not ban on hosting alone. Do not treat every MOB address as a farm.

Also leave crawler alone: search-engine exits are benign and do not participate in scoring. Cloud-phone farms are a different problem from Googlebot.

Why device signals are not enough

Farms buy the stack you already trust: real Android images, real WebViews, residential-looking fingerprints sold as a service. Your client SDK reports "phone." Your fraud model sees mobile-shaped behavior. The cheap tell that survives that arms race is often where the TCP session comes from and whether that egress has been observed as cloud-phone infrastructure.

That observation expires. An address used by a farm yesterday may be quiet today. IP99's product stance is a judgement that expires: scores decay with evidence age; evidence_state tells you whether anything fresh is still on the board.

Triage order (same as every other IP99 gate)

  1. evidence_state — can this conclusion be trusted now? active = fresh evidence scoring; stale = seen before, aged out, score 0 (weak feature only); none = never seen (coverage gap), not "safe."
  2. risk.score / risk.level — only after you read state. 0 with none is not a clean bill of health.
  3. risk.signals — why it scored. cloud_phone is a stronger farm smell than bare hosting. hijacked_proxy still means the line owner is likely a victim — step up, don't hard-ban the household forever.
  4. Share scope — Pulse share_tier (T0–T3) if you have professional history; on free responses use usage_type (IDC vs DYN / MOB) as a rough prior before you equate one ban with one building.
  5. degraded: true — discount the answer when sources are partial.

Official disposition guidance (generic, not a contract promise): score 0 → do not stamp "clean"; 1–29 → log/observe; 30–69 → verify (CAPTCHA, second factor, rate limit, demote) rather than hard-ban; 70–100 → block only when business cost allows, and still respect share scope.

Live curls (2026-10-28, no API key)

Anonymous quota: 500 lookups/day per client IP, 60/min. Sign in for 5,000/day. Overseas self-serve Pro / Pro+ ($29 / $99) buys volume, not a different score — official line: you pay for volume, not a better answer. Per-item evidence rows, share_tier, and ?at= history are a separate professional / contracted Pulse capability.

# Datacenter control: hosting attribute, score 0 — not a farm by itself
curl https://ip99.com/v1/ip/8.8.8.8

# Docs demo residential address — mask the last octet in tickets/logs
curl https://ip99.com/v1/ip/27.22.66.200

# Remaining anon quota
curl https://ip99.com/v1/me
Enter fullscreen mode Exit fullscreen mode

Sample A: hosting is not cloud_phone

Captured ~2026-10-28 13:10 UTC:

{
  "ip": "8.8.8.8",
  "computed_at": "2026-10-28T13:10:41Z",
  "evidence_state": "none",
  "geo": {
    "country": "US",
    "region": "California",
    "city": "Mountain View",
    "tz": "America/Los_Angeles"
  },
  "network": { "asn": 15169, "usage_type": "IDC" },
  "risk": {
    "score": 0,
    "level": "none",
    "signals": ["hosting"]
  }
}
Enter fullscreen mode Exit fullscreen mode

usage_type: IDC plus hosting describes where the address lives. There is no cloud_phone here. A rule that bans every IDC egress will also ban DNS resolvers, CI runners, and your own monitoring.

Sample B: residential churn ≠ handset farm

Same window; last octet shown because this is IP99's documented demo address — still mask real customer IPs in your tickets:

{
  "ip": "27.22.66.200",
  "computed_at": "2026-10-28T13:10:44Z",
  "degraded": true,
  "evidence_state": "active",
  "geo": {
    "country": "CN",
    "region": "Hubei",
    "city": "Xiangyang",
    "district": "Yicheng",
    "tz": "Asia/Shanghai"
  },
  "network": { "asn": 4134, "usage_type": "DYN" },
  "risk": {
    "score": 17,
    "level": "low",
    "signals": ["dialup_pool", "hijacked_proxy", "proxy"]
  }
}
Enter fullscreen mode Exit fullscreen mode

This is residential broadband with dial-up-pool / proxy evidence — the second-dial / shared-exit problem, not a rack of remoted phones. Disposition differs: hijacked_proxy argues for step-up and careful appeals, not "delete the building." When your gate does see cloud_phone (often with hosting-side attributes), treat it as a farm-shaped smell: challenge identity, tighten device binding, delay payout — still prefer verify-over-ban on medium scores unless your loss model says otherwise.

A minimal signup / claim hook

# At registration or high-value claim — server-observed client IP only
IP="$CLIENT_IP"   # from the reverse proxy, never from a JSON body field
RESP=$(curl -sS "https://ip99.com/v1/ip/${IP}")

# Pseudocode:
# state = RESP.evidence_state
# signals = RESP.risk.signals or []
# if state == "active" and "cloud_phone" in signals:
#     step_up(challenge="phone_otp_or_manual_review")
# elif state == "active" and RESP.risk.score >= 70:
#     decide_with_share_scope(...)
# elif state in ("stale", "none") and score == 0:
#     do_not_mark_clean(); continue_normal_friction()
Enter fullscreen mode Exit fullscreen mode

Persist computed_at, evidence_state, score/level, and signals with the decision so chargeback week can replay what you knew — the free /v1/ip endpoint answers now only; historical ?at= needs Pulse.

What this article is not claiming

  • IP99 does not ship a Tor-specific signal; do not market one.
  • Risk score is not attacker probability.
  • Free /v1/ip and professional Pulse share one score for one IP; Pulse adds evidence detail and optional history, not a second verdict.
  • IP99 and ThreatHunter datasets are related products under the same company — do not claim they are identical feeds.
  • No invented accuracy rates, customers, or funding numbers appear above. Mask real residential or mobile subscriber IPs in anything you publish or paste into tickets.

Try it

Open ip99.com, paste an egress, and read risk.signals before you trust a "mobile" client claim. Or:

curl https://ip99.com/v1/ip/8.8.8.8
Enter fullscreen mode Exit fullscreen mode

API shape, quotas, and field semantics: ip99.com/api. When cloud_phone shows up active on a signup that already looked like a handset, you finally have a name for the farm — and a score that will forget it when the evidence goes stale.

Top comments (0)