Score Zero Has Two Meanings: Branch on evidence_state First
If your antifraud rule treats every risk.score == 0 as "clean," you will misread two very different situations the same way. On IP99 by ThreatHunter, score zero can mean never seen (evidence_state: none) or seen before,
If your antifraud rule treats every risk.score == 0 as "clean," you will misread two very different situations the same way. On IP99 by ThreatHunter, score zero can mean never seen (evidence_state: none) or seen before, evidence window expired (evidence_state: stale). Both print 0. Only one of them is a coverage gap; the other is a fading memory with leftover signal slugs. The documented read order starts with evidence_state for that reason.
This walkthrough stays on the free, no-key GET /v1/ip/{ip} endpoint. Live samples were captured on 2026-11-03 (~13:11 UTC).
What evidence_state actually means
IP99's risk score is freshness and strength of verifiable risk evidence, on a 0โ100 scale that decays as evidence ages. It is not the probability that an address is an attacker. risk.level is derived from the score: 0 โ none, 1โ29 โ low, 30โ69 โ medium, 70โ100 โ high.
evidence_state tells you whether that score is even in play:
evidence_state |
Meaning | Score behavior |
|---|---|---|
active |
Fresh enough evidence to participate in scoring | Score can be 1โ100 |
stale |
Address was seen before; evidence window expired | Forced to 0; leftover signals are a weak feature |
none |
Never seen in the risk evidence set | Also 0; coverage gap โ not "safe" |
Official disposition guidance (generic advice, not a contract): score 0 โ do not mark the IP clean; 1โ29 โ observe; 30โ69 โ step-up (captcha, throttle, second factor) rather than a hard ban; 70โ100 โ block only when business cost allows. hijacked_proxy should not automatically ban the account holder โ that address is often a victim of malware-turned-exit capacity.
Example A: score 0 + none (never seen as risk evidence)
curl https://ip99.com/v1/ip/8.8.8.8
Captured 2026-11-03T13:11:32Z:
{
"ip": "8.8.8.8",
"computed_at": "2026-11-03T13:11:32Z",
"evidence_state": "none",
"geo": {
"continent": "NA",
"country": "US",
"region": "California",
"city": "Mountain View",
"district": "",
"tz": "America/Los_Angeles"
},
"network": { "asn": 15169, "usage_type": "IDC" },
"risk": {
"score": 0,
"level": "none",
"signals": ["hosting"]
}
}
usage_type: IDC and signals: ["hosting"] are network attributes, not a proxy verdict โ Google Public DNS lives in hosting space. evidence_state: none with score 0 means IP99 holds no current verifiable risk evidence for this address. That is not a clean bill of health; it is "we have nothing risk-shaped on file." Do not invent Tor or other signals that are not in the response.
Example B: score 0 + stale (same demo IP, different day)
Public docs often demonstrate with a residential dial-up / proxy-supply address. Full address works in curl; prose and tickets should mask the last octet.
curl https://ip99.com/v1/ip/27.22.66.200
Captured 2026-11-03T13:11:48Z (last octet masked in the body):
{
"ip": "27.22.66.x",
"computed_at": "2026-11-03T13:11:48Z",
"evidence_state": "stale",
"geo": {
"continent": "AS",
"country": "CN",
"region": "Hubei",
"city": "Xiangyang",
"district": "Yicheng",
"tz": "Asia/Shanghai"
},
"network": { "asn": 4134, "usage_type": "DYN" },
"risk": {
"score": 0,
"level": "none",
"signals": ["dialup_pool", "hijacked_proxy", "proxy"]
}
}
Same score shape as Example A โ 0 / none โ but a different story. usage_type: DYN is residential broadband. The signal list still carries proxy, dialup_pool, and hijacked_proxy. On 2026-11-01, this same demo address returned evidence_state: active with a low non-zero score. Today the window has expired: score is forced to 0, state is stale, and those slugs survive as a weak feature โ useful for logging and soft routing, not as a fresh "ban now" trigger.
If your code only checks score == 0, Examples A and B collapse into one bucket. Branch on evidence_state first and they stay distinct: A is a gap; B is expired evidence that still names the historical observation types.
The decision order (free /v1/ip)
IP99's documented order is intentionally boring:
-
evidence_stateโ is the conclusion even scorable right now? -
risk.score/risk.levelโ how strong/fresh is the evidence whenactive? -
risk.signalsโ branch by type (proxy/vpn/dialup_pool/cloud_phonevscrawlervs network attributeshosting/mobile). -
Ban scope โ on Pulse,
share_tier(T0โT3) decides whether an IP ban hits one tenant or a whole CGNAT building. The free/v1/ipresponse does not includeshare_tier; keep that in mind before hard-blocking. -
degradedโ if present, some upstreams were unavailable; discount the answer.
A small branch that matches the schema:
if evidence_state == "none" and score == 0:
do_not_mark_clean() # coverage gap
elif evidence_state == "stale" and score == 0:
log_weak_feature(signals) # expired memory; soft weight only
elif evidence_state == "active":
if score >= 70:
block_if_business_allows()
elif score >= 30:
step_up() # captcha / throttle / review
else:
observe()
# hosting / mobile in signals โ network context only; never sole ban reason
# hijacked_proxy โ step-up, not automatic account ban
Anti-patterns this avoids: treating every zero as trusted; caching yesterday's label without storing computed_at + evidence_state; equating hosting with fraud; hard-banning on hijacked_proxy alone.
Try it without a key
curl https://ip99.com/v1/ip/8.8.8.8
curl https://ip99.com/v1/ip/27.22.66.200
curl https://ip99.com/v1/ip # caller's own egress
curl https://ip99.com/v1/me # quota / plan headers context
Anonymous quota is 500 lookups/day per client IP and 60/min, resetting at midnight Asia/Shanghai. CORS is open (Access-Control-Allow-Origin: *). Machine-readable contract: OpenAPI. Field deep-dives: evidence freshness, risk score, detect proxy/VPN, and the English API reference.
Paid volume plans change quotas, not the free answer shape. Per-evidence rows, share_tier, and ?at= history sit on Pulse (professional).
Takeaway
Score zero is ambiguous until you read evidence_state. none means no risk evidence on file. stale means evidence existed, the scoring window expired, and leftover signal slugs are weak features โ not a fresh verdict. Branch on state first, then score, then signal type. For the free lookup and field definitions, start at ip99.com.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.