DEV Community

IP99
IP99

Posted on

What Did We Know at Chargeback Time? Replaying IP Risk with a Timestamp

A chargeback lands weeks after checkout. Your fraud queue opens the order, copies the billing IP, and hits today's IP reputation API. The score is 0. Someone stamps the dispute "clean egress" and moves on.

That workflow asks the wrong question. IP risk on IP99 by ThreatHunter is a judgement that expires: the score measures freshness and strength of verifiable evidence, not "probability this cardholder is an attacker." An address that looked like a dial-up proxy exit at purchase may show evidence_state: none and score 0 by the time Finance asks. Re-querying now does not reconstruct then.

Live gate vs chargeback clock

Moment Question Free GET /v1/ip/{ip} Pulse GET /pulse/v1/ip/{ip}?at=
Checkout / claim Is this egress risky right now? Yes — no key required Same score, more evidence detail
Chargeback / audit What did we know at order time? No — at returns 400 at_unsupported Yes — professional tier + history enabled, ~30 days back

Official product split: basic endpoint for real-time control (sign-up, sign-in, pre-order); Pulse for after-the-fact review, reconciliation, and strategy back-testing. One IP, one score — Pulse is not a "better verdict," only a deeper and time-travel view of the same judgement.

What to store at decision time (so replay is possible)

If you only keep the final allow/deny bit, chargeback week has nothing to replay. At the gate that matters (checkout, claim, payout), persist at least:

  1. Client egress IP (server-observed — never trust a body field the browser sent).
  2. computed_at from the IP99 response (RFC 3339).
  3. evidence_state, risk.score / risk.level, and risk.signals (or "signals absent").
  4. network.asn and network.usage_type for blast-radius context when you lack Pulse share_tier.

That snapshot is your audit trail on the free tier. Historical ?at= later lets you re-ask the feed as of that moment; it does not replace logging what your gate actually used.

How to read a replay (same triage order)

Whether you look at the stored snapshot or a Pulse history response, official reading order still applies:

  1. evidence_state — can this conclusion be trusted at that moment? none means never seen (coverage gap), not "safe." stale means seen before but evidence aged out (score 0, weak feature only).
  2. risk.score / risk.level — only meaningful once state is active or stale.
  3. risk.signals — why it scored: proxy, vpn, dialup_pool, cloud_phone, hijacked_proxy, … matter differently for dispute letters than bare hosting (a network property — Google Public DNS carries hosting with score 0).
  4. Share scope — Pulse share_tier (T0–T3) if you ever blocked; on free responses use usage_type (DYN / MOB vs IDC) as a rough prior. Do not equate one residential NAT ban with one exclusive VPS ban.
  5. Degraded marker — basic body degraded: true, or Pulse header X-Pulse-Degraded: 1 — discount the answer.

hijacked_proxy still means the line owner is likely a victim; a chargeback write-up that says "banned the household forever" is usually the wrong story even when proxy evidence was active at purchase.

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

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

# What the free API answers: now only
curl https://ip99.com/v1/ip/8.8.8.8

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

# Remaining anon quota (does not spend a lookup on some setups; still cheap to check)
curl https://ip99.com/v1/me

# History needs Pulse + professional key — free /v1/ip rejects at=
# curl -H "X-API-Key: YOUR_KEY" \
#   "https://ip99.com/pulse/v1/ip/86.111.136.23?at=2026-09-14T09:29:23Z"
Enter fullscreen mode Exit fullscreen mode

Sample A: datacenter control (score 0 is not a chargeback win)

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

{
  "ip": "8.8.8.8",
  "computed_at": "2026-10-27T13:13:13Z",
  "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

hosting here is network shape, not proxy proof. evidence_state: none with score 0 means IP99 holds no current verifiable risk evidence — not that the payment was legitimate.

Sample B: residential dial-up / proxy supply (mask last octet)

Same day live lookup. Full address works in the curl above; tickets should show 27.22.66.x.

{
  "ip": "27.22.66.x",
  "computed_at": "2026-10-27T13:13:15Z",
  "evidence_state": "active",
  "geo": {
    "country": "CN",
    "region": "Hubei",
    "city": "Xiangyang",
    "district": "Nanzhang",
    "tz": "Asia/Shanghai"
  },
  "network": { "asn": 4134, "usage_type": "DYN" },
  "risk": {
    "score": 18,
    "level": "low",
    "signals": ["dialup_pool", "hijacked_proxy", "proxy"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Still DYN home broadband; signals say dial-up pool + proxy presence; hijacked_proxy warns collateral. active + low is friction territory at checkout — and the score will move as evidence ages. If you only re-check this IP at chargeback time without a stored computed_at, you may see a different story and wrongly conclude the gate was wrong.

Docs also show the same idea on Pulse history: one Luxembourg residential proxy exit scored 90 at a named past moment and 77 later — same address, same feed, evidence aged. That drift is intentional.

Minimal gate sketch (store the snapshot)

import json, urllib.request
from datetime import datetime, timezone

def lookup_now(ip: str) -> dict:
    req = urllib.request.Request(f"https://ip99.com/v1/ip/{ip}")
    with urllib.request.urlopen(req, timeout=5) as r:
        return json.load(r)

def snapshot_for_order(ip: str) -> dict:
    d = lookup_now(ip)
    return {
        "egress_ip": d["ip"],
        "computed_at": d["computed_at"],
        "evidence_state": d["evidence_state"],
        "score": d["risk"]["score"],
        "level": d["risk"]["level"],
        "signals": d["risk"].get("signals"),
        "asn": d.get("network", {}).get("asn"),
        "usage_type": d.get("network", {}).get("usage_type"),
        "stored_at": datetime.now(timezone.utc).isoformat(),
    }

# At chargeback: compare dispute narrative to snapshot_for_order(...).
# To re-ask the feed as of order time, use Pulse ?at= with a professional key —
# do not pretend free /v1/ip can time-travel.
Enter fullscreen mode Exit fullscreen mode

CORS is open on /v1/ip, but run the gate on your server. Import https://ip99.com/openapi.json into Postman or codegen if you want a typed client. Agents can call the same live lookup as MCP tool ip_risk_lookup at https://ip99.com/mcp.

Honest limits

  • Score is not attacker probability; 0 / evidence_state: none is not "safe."
  • Free /v1/ip answers now. Passing at yields at_unsupported.
  • Pulse ?at= needs professional access and history enabled separately; history indexes IPv4 in the documented window (~30 days). Do not invent free historical fields.
  • IP99 and ThreatHunter branding: IP99 is the product; do not claim the free API and every ThreatHunter dataset are identical.
  • Mask the last octet of real residential or mobile addresses in public tickets and posts.

Try it

Paste a checkout egress into ip99.com or run the free curls above. For live gates, branch on evidence_state + typed signals and store computed_at with the order. For chargebacks, argue from that snapshot — and use Pulse history when you truly need "what did the feed say at purchase," not today's re-score of a recycled address.

Top comments (1)

Collapse
 
challan116ux profile image
challan116-ux •

Replaying with the timestamp is the whole game — evaluating a past decision with present data quietly convicts people for information nobody had at the time. Keeping the decision-time snapshot, inputs and all, turns a chargeback review from an argument about memory into a check of a record, and it also protects the opposite case: a decision that looks wrong today but was correct on what was knowable then. The same discipline applies anywhere money moves on evidence — what matters in a dispute is never what the system knows now, it is what it can prove it knew when it acted.