Account Warmup and Identity Lifecycle
A New Account Is the Loudest Thing You Can Send
Every other signal you send can be tuned. Account age cannot. A credential created forty seconds ago that starts hitting a price endpoint is not a misconfigured browser, it is a fact, and the defender's ledger has no field you can override. Which is why authenticated work starts with the account's history rather than the account's requests.
What the Age Signal Is Actually Worth
An account's age is the hardest signal to forge because it is corroborated by things you do not control. Registration carries a timestamp from a server you did not choose, sitting in a row that also records the email domain's age, the payment instrument's history, the IP's first-seen date, and the ASN's first-seen date. You can backdate the visible field; you cannot backdate six corroborating ones.
The weight is asymmetric. Going from zero to thirty days is worth more than going from thirty to three hundred. Most account-level models are close to a saturating curve: fast early gain, long flat tail, with the first two weeks carrying most of the available signal. An account you aged by four hundred days and one you aged by thirty days are much closer than either is to zero, which is the useful thing to know when you are deciding whether to keep investing in an identity.
There is a second component that gets called age and is not: how long the credential has been used consistently from a stable set of networks. A ninety-day-old credential that has appeared from forty residential addresses in three days is younger than its creation date suggests. Treat age and continuity as separate columns in your own model, because they fail separately and you will need to attribute a ban to one of them.
Cadence Is Where Machines Are Obvious
If age is a fact about the past, cadence is a fact about the shape of your traffic, and it is the field a naive automation always gets wrong. Measure the coefficient of variation of your daily action counts: the standard deviation over the mean. A person browsing a product site on weeknights produces something like cv between 0.6 and 1.4, with weekends higher. A script on a fixed schedule produces cv near 0.05.
That gap is enormous and it costs nothing to close. Real daily counts are zero-heavy: several days of nothing, then a burst. Real inter-arrival times inside a session are not uniform either, and a sleep(2.0) between pages is a signature in itself, which is why waf evasion tips spends a section on jitter. What matters is not the distribution you pick but that it has the right properties: a heavy tail, occasional long gaps, and no periodic structure a Fourier transform could find.
Add the second-order features, because the first-order ones get patched. The ratio of unique URLs to total requests per session. The share of requests that are navigations versus subresources. Whether the sequence is consistent with a workflow: search, list, detail, detail, detail, cart. And whether the click-path has backtracking, which people do and scripts do not.
What a Believable History Contains
Five properties, and you can check all of them offline.
- A long quiet tail. Most of the account's life is low activity. The shape is bursty, not linear.
- Repeats. Real users revisit the same handful of pages. A crawler that visits 400,000 URLs and revisits none is trivially separable from a person regardless of how good its timing is.
- A consistent identity. The same device, the same browser version, the same timezone, the same language, the same fonts, the same viewport, across weeks. Device profile consistency is not optional here, because an account whose device changed on the 90th day and again on the 200th has two independent reasons to be doubted.
- Occasional failures. Password resets, expired cards, a support ticket, a failed login from a phone. An account with a flawless record for a year is not more trustworthy than one with three human incidents in it.
- Time away. Multi-day gaps that do not align with anyone's working hours, plus one or two multi-week absences. Distributed worker fleets is where you end up if you treat a fleet of accounts as a fleet of workers, and that framing is what makes these properties schedulable.
None of this requires volume. It requires patience and a bookkeeping habit: a per-account log of what was done, when, from where, and which device, kept so you can reconstruct what the defender could reconstruct.
Why Bought Accounts Fail
An aged account is a hollow shell. What you are buying is a creation timestamp, and the ledger is comparing it against everything else the account carries.
Payment instrument history. The card that funded the account has its own age and its own velocity. A four-year-old account settled with a card opened nine days ago is a contradiction the defender can see without any of your traffic. Cards bought alongside accounts share issuer, BIN and often shipping address; a batch of accounts on one card is one graph edge away from a fraud cluster.
Device history. The account's first sessions left a device. If that device id, canvas hash, or client-hint tuple has since appeared on fifty other accounts, the account is now part of a cluster regardless of how carefully you warmed it. This is the failure that bites hardest because it is retroactive: the warm-up you did at purchase is now a shared fingerprint.
Behavioral history that cannot be reconciled. Accounts bought in volume come from sellers who ran a generation script. If 200 accounts were each registered from the same ASN, within the same 20-minute window, with the same UA, the same viewport and the same registration path, they are not 200 aged accounts; they are one batch operation with a purchase date. Their cadence variance is the variance of a script, not of a person.
The email domain. Aged-account sellers recycle domains. A mailbox domain created last Tuesday, or a plus-addressed sub-mailbox, dates the account regardless of what the row says.
Cross-check on the credential itself. If the password was ever reused from a breach corpus that the defender holds, the age of that corpus entry is public information. Password reuse on an old account is the single most common way a genuinely old account is devalued instantly.
The only version of this that works is building the account, which is the warmup discipline the rest of this lesson describes, and it is slower than buying. It is also the version that survives contact with a risk score and session profile that actually reads the corroborating fields.
How a Defender Graphs One Identity Across Many Accounts
The graph is the part that makes account-level defenses work, and it is the same structure anomaly models look for in the traffic rather than in the account.
Vertices are accounts, and also devices, credentials, payment instruments, email domains, subnets and ASNs. Edges are shared attributes. Then the graph is queried for structure, not for rules:
- Cohorts. Accounts that share a device hash, a client-hint tuple, a canvas hash, or a WebGL renderer string. A single renderer string across 200 accounts is a signature even when every other field is randomised.
- Stack overlap. Accounts sharing an
(OS build, ASN, locale, timezone)tuple. Each element alone is common; the four-tuple is not, and you can compute its population size yourself before you deploy. - Cohesion. A subgroup whose members are more connected to each other than to the population is a cluster, and the denser the cluster the more likely it is a ring. Real accounts overlap with the population a little; a ring does not overlap at all.
- Temporal coupling. Accounts whose action timestamps cluster within minutes of each other. Ten accounts acting within the same ninety-second window, repeatedly, is a scheduler, and no amount of per-account jitter hides it because the coupling is between accounts.
The defensive move is to raise the cost of every edge you want to forge. Doubling device diversity halves the value of each device. Varying the exit ASN per account breaks the stack edge. Introducing a genuine spread of activity times breaks the temporal edge. Doing none of these and instead buying age buys a timestamp.
Scoring a Fleet and Flagging the Cluster
Here is the arithmetic, on eight accounts. Age contributes up to 40 points on a saturating curve, cadence variance up to 22, device reuse divides 20, and shared (os, asn) stack divides 18. A floor at 45 is where an account stops being treated as a person.
# How a defender scores a fleet of accounts and graphs them into one identity.
# Fixed inputs, arithmetic only, no clock and no randomness.
ACCOUNTS = [
{"id": "acct-01", "age_days": 412, "device": "dev-9f2", "os": "win11-24H2",
"asn": "AS64500", "region": "de", "logins": 61, "actions": [3, 0, 9, 1, 14, 0, 6]},
{"id": "acct-02", "age_days": 388, "device": "dev-9f2", "os": "win11-24H2",
"asn": "AS64500", "region": "de", "logins": 58, "actions": [4, 0, 8, 2, 12, 1, 5]},
{"id": "acct-03", "age_days": 401, "device": "dev-9f2", "os": "win11-24H2",
"asn": "AS64500", "region": "de", "logins": 60, "actions": [2, 1, 11, 0, 15, 0, 4]},
{"id": "acct-04", "age_days": 30, "device": "dev-4a1", "os": "macos-15.3",
"asn": "AS64500", "region": "de", "logins": 4, "actions": [40, 40, 41, 40, 40, 39, 40]},
{"id": "acct-05", "age_days": 31, "device": "dev-4a1", "os": "macos-15.3",
"asn": "AS64500", "region": "de", "logins": 4, "actions": [40, 40, 40, 40, 41, 40, 40]},
{"id": "acct-06", "age_days": 29, "device": "dev-4a1", "os": "macos-15.3",
"asn": "AS64500", "region": "de", "logins": 3, "actions": [40, 41, 40, 40, 40, 40, 40]},
{"id": "acct-07", "age_days": 205, "device": "dev-77c", "os": "win11-23H2",
"asn": "AS3320", "region": "us", "logins": 22, "actions": [7, 2, 0, 11, 5, 0, 9]},
{"id": "acct-08", "age_days": 44, "device": "dev-b30", "os": "ios-18.4",
"asn": "AS3320", "region": "us", "logins": 2, "actions": [31, 30, 32, 30, 30, 31, 30]},
]
def mean(xs):
return sum(xs) / len(xs)
def stdev(xs):
m = mean(xs)
return (sum((x - m) ** 2 for x in xs) / len(xs)) ** 0.5
def age_points(days):
# 0 days -> 0, 30 -> 12, 90 -> 24, 365 -> 40, flat after a year
if days <= 0:
return 0.0
if days <= 30:
return 40.0 * days / 90.0
if days <= 90:
return 40.0 * (12.0 + (days - 30) * 0.4) / 40.0
return min(40.0, 24.0 + (days - 90) * 0.0616)
def cadence_points(actions):
# Humans miss days and spike. A constant daily count scores near zero.
m = mean(actions)
if m <= 0:
return 0.0
cv = stdev(actions) / m
if cv < 0.15:
return 0.0
if cv > 0.90:
return 22.0
return 22.0 * (cv - 0.15) / 0.75
def reuse_index(acct, pool):
device = 1 + sum(1 for o in pool
if o is not acct and o["device"] == acct["device"])
stack = 1 + sum(1 for o in pool if o is not acct
and o["os"] == acct["os"] and o["asn"] == acct["asn"])
return device, stack
def trust(acct, pool):
age = age_points(acct["age_days"])
cad = cadence_points(acct["actions"])
device_n, stack_n = reuse_index(acct, pool)
device = 20.0 / device_n
stack = 18.0 / stack_n
# A fresh account cannot be trusted no matter how the rest scores.
return max(0.0, min(100.0, age + cad + device + stack)), \
(age, cad, device, stack)
def main():
pool = ACCOUNTS
rows = []
for a in pool:
score, parts = trust(a, pool)
rows.append((a, score, parts))
print("per-account trust, 100 points max")
print(" id age dev os/asn share cv age cad dev stk trust")
for a, score, (age, cad, dev, stk) in rows:
cv = stdev(a["actions"]) / mean(a["actions"])
device_n, stack_n = reuse_index(a, pool)
print(f" {a['id']} {a['age_days']:>4} {device_n:>3} {stack_n:>8} "
f"{cv:4.2f} {age:4.1f} {cad:4.1f} {dev:4.1f} {stk:4.1f} "
f"{score:5.1f}")
print()
print("graph: accounts sharing one device id, one os+asn stack, or both")
by_device, by_stack = {}, {}
for a in pool:
by_device.setdefault(a["device"], []).append(a["id"])
by_stack.setdefault((a["os"], a["asn"]), []).append(a["id"])
flagged = set()
for dev in sorted(by_device):
ids = sorted(by_device[dev])
if len(ids) < 2:
continue
stack = {k: sorted(v) for k, v in by_stack.items() if set(v) >= set(ids)}
print(f" device {dev}: {len(ids)} accounts {ids}")
for k in sorted(stack):
print(f" all on {k[0]} / {k[1]}")
flagged.update(ids)
print(f" flagged {len(flagged)} of {len(pool)}: {sorted(flagged)}")
cold = [a["id"] for a, s, _ in rows if s < 45]
asns = sorted({a["asn"] for a in pool})
devices = sorted({a["device"] for a in pool})
print()
print(f" below the 45-point automation floor: {sorted(cold)}")
print(f" pool shape: {len(pool)} accounts, {len(devices)} device ids, "
f"{len(asns)} asns ({', '.join(asns)})")
print(f" verdict: {len(flagged)} accounts collapse into "
f"{len(devices) - 1} shared devices; nothing about the account "
f"objects separates them")
main()
per-account trust, 100 points max
id age dev os/asn share cv age cad dev stk trust
acct-01 412 3 3 1.04 40.0 22.0 6.7 6.0 74.7
acct-02 388 3 3 0.86 40.0 20.8 6.7 6.0 73.4
acct-03 401 3 3 1.17 40.0 22.0 6.7 6.0 74.7
acct-04 30 3 3 0.01 13.3 0.0 6.7 6.0 26.0
acct-05 31 3 3 0.01 12.4 0.0 6.7 6.0 25.1
acct-06 29 3 3 0.01 12.9 0.0 6.7 6.0 25.6
acct-07 205 1 1 0.83 31.1 20.1 20.0 18.0 89.1
acct-08 44 1 1 0.02 17.6 0.0 20.0 18.0 55.6
graph: accounts sharing one device id, one os+asn stack, or both
device dev-4a1: 3 accounts ['acct-04', 'acct-05', 'acct-06']
all on macos-15.3 / AS64500
device dev-9f2: 3 accounts ['acct-01', 'acct-02', 'acct-03']
all on win11-24H2 / AS64500
flagged 6 of 8: ['acct-01', 'acct-02', 'acct-03', 'acct-04', 'acct-05', 'acct-06']
below the 45-point automation floor: ['acct-04', 'acct-05', 'acct-06']
pool shape: 8 accounts, 4 device ids, 2 asns (AS3320, AS64500)
verdict: 6 accounts collapse into 3 shared devices; nothing about the account objects separates them
Read the last section before the table. Three accounts on macos-15.3 through AS64500 have cadence scores of exactly zero, and three on win11-24H2 through the same ASN score 74 with a coefficient of variation above 0.85. The graph flags all six, because they collapse into two device identities. The two accounts the score alone is most suspicious of -- acct-04 through acct-06, new and metronomic -- are the two the graph is least worried about in isolation, and the confident-looking ones on dev-9f2 are the ones the graph kills. Neither view is sufficient on its own.
Operating an Identity Over Its Lifetime
Treat every credential as a state machine with four states and explicit promotion rules.
- Cold. Registered, no meaningful history. Used for nothing that matters. Costs almost nothing to hold.
- Maturing. Some history, moderate trust. Takes browsing-level traffic only.
- Mature. Long history, stable device, stable exit region. Carries the real work, absorbs the mistakes a mature ledger can afford.
- Retired. Banned, challenged repeatedly, or showing a climbing score. Stopped immediately, not cooled down and reused.
The value of this framing is that it makes rotation a scheduling problem rather than a panic response. You promote on a calendar, you demote on the first soft-challenge, and you keep the mature tier large enough that losing one is a scheduling hiccup rather than an outage. A fleet with only mature identities and no pipeline into it is one vendor change away from having nothing.
Track four numbers per identity and re-read them hourly: age, days since last use, cumulative actions, and challenge-to-success ratio over the last hundred requests. Everything else is derived.
What Actually Ages an Identity
Concrete, and in rough order of return.
- Browsing the target on a schedule. Reading, searching, and a low rate of detail views, from one stable region, for weeks. This is the whole trick and it is mostly waiting.
- Occasional real intent. An account that has added something to a cart, saved a payment method, or written a review is in a different population from one that only reads. Do it rarely and do it plausibly.
- A real device lineage. One persistent profile directory that accumulates history. Browser pool architecture gives you the eviction policy that keeps this from being replaced on a schedule.
- Coherent failures. A password reset, a failed payment, one 401. A history with no incidents has no incidents because it is not a life.
- A stable egress identity. One residential exit, or a small set in one metro, for the account's whole life. Rotating the exit every session resets the weakest corroborating field you have.
What does not help: more actions, faster growth, a longer scripted warm-up script, or a second account doing the same thing. Volume actively subtracts, because the marginal action is more suspicious than the median one, and two accounts warming in lockstep is the temporal-coupling edge.
Keeping the Model Honest
Models drift, and a warmup strategy tuned against last quarter's model is a liability. Three habits keep it current.
- Re-derive the thresholds from your own observations. Your score distribution, your measured challenge rate at each cohort age. If an account aged 20 days challenges at 4 percent and one aged 200 challenges at 3 percent, the marginal value of the second 180 days is a third of the first 20 and you should spend that waiting time elsewhere.
- Track the ban rate per age cohort. The cohort that banned hardest is where your model is wrong, and it is usually the cohort you were most confident about.
- Re-read the signals, not the marketing. Keeping current exists because vendor behaviour moves; a warmup policy built on a six-month-old blog post is a policy for a target that no longer exists.
The uncomfortable summary is that you cannot warm an account quickly, and every attempt to do so is visible because the field being forged is the field with the most corroboration. Age the account, or target something that does not require an account.