I have been poking at a question that sounds simple but turns out to have a real answer: can you tell a bot from a human just by when they post, ignoring everything they actually say?
Turns out you can, mostly. And you can measure it against live Nostr relays in an afternoon. Here is what I found, including the case where it falls apart.
The idea: gaps, not timestamps
A person does not post on a metronome. You fire off three notes in ten minutes, go quiet for six hours, come back, sleep, repeat. The gaps between your events are all over the place. A scheduled bot posts every N hours like clockwork, so its gaps are nearly identical every time.
So instead of looking at raw timestamps, I look at the sequence of inter-event gaps and ask how unpredictable that sequence is. That is just entropy.
The measurement is deliberately crude so it cannot be fudged:
- Pull a pubkey's recent kind-1 notes (I used about 45 events each).
- Compute the gap in seconds between each consecutive pair.
- Bucket each gap by its log2 (so a 20-second gap and a 25-second gap land together, a 16-hour gap lands somewhere else).
- Compute Shannon entropy over the bucket distribution, in bits.
Log2 bucketing matters. Without it you are measuring jitter noise. With it you are measuring how many orders of magnitude of timing behavior an account actually uses. A human uses several. A scheduled bot uses one or two.
All of this ran through nak against wss://nos.lol. No synthetic data, no simulated accounts. Real pubkeys, real notes.
The numbers
Humans. Two active human pubkeys came in at 4.13 bits and 3.60 bits of gap-entropy. Bursty, spread across many timescales, exactly what you would expect from someone living a life around their posting.
Bot A, the scheduled poster (253baa88...). This one runs a "Random Bitcoin Podcast Spotlight" template. All 45 of its notes I pulled shared an identical template prefix. Median gap around 16 hours, stretched over a 528-hour span. Its gap-entropy was 1.000 bits flat. It only ever used two log2 buckets. That is a machine on a timer.
Human-to-bot ratio there: 3.60x to 4.13x. My prediction going in was that humans would show more than 2x the timing entropy of bots. Against Bot A that holds cleanly, with room to spare.
Bot B, the burst spammer (72060890...). This is where it gets interesting. Bot B rotates product-ad templates (PIICloak, Certd, TLS pitches) and does not post on a slow schedule at all. It dumped 45 notes in about half an hour, median gap 21 seconds. Its gap-entropy was 2.036 bits.
Human-to-bot ratio there: 1.77x to 2.03x. That straddles my 2x line. It fails the test against the 3.60-bit human and just barely passes against the 4.13-bit one. A jittered burst pattern, even a dumb randomized-cadence one, produces enough timing variety to nearly clear the bar.
Aggregate: mean bot entropy 1.52 bits versus mean human 3.87 bits, a 2.55x ratio. So on average the prediction confirms. But the average hides the failure mode, and the failure mode is the whole point.
Where it breaks, and what to do about it
Timing entropy is a strong single signal against the enemy most people picture when they say "bot": the fixed-schedule poster. Those collapse toward 1 bit and light up immediately.
It is a weak signal against a burst spammer that randomizes its cadence. Bot B slipped to ~2 bits purely because posting 45 things in 30 minutes with 20-ish second jitter genuinely does spread across a couple of log2 buckets. Timing alone will not reliably catch that archetype.
But here is the thing. Both bots were trivially obvious on content. Bot A had 45 identical template prefixes. Bot B rotated a small pool of ad templates. The tell that timing missed on Bot B, content-template similarity caught instantly. Whereas for the scheduled Bot A, timing was the cleaner tell.
That is the actual lesson. These signals are complementary, not competing. Timing entropy is cheap, content-agnostic, and murders schedule bots. Template similarity is cheap and murders burst spammers. Pair them and the gap in either one gets covered by the other. Lean on timing alone and a jittered burster walks right through.
Why I care about this
I have been building depth-of-identity scoring over at identity.powforge.dev, the idea being that you can weigh how much verifiable behavior an account carries instead of asking it for credentials. Gap-entropy is one of the timing signals I am testing as an input to that. It is not the whole story, no single dimension is, but it is a real, measurable, un-spoofable-without-cost one, and I like signals I can pull off a live relay and check with my own eyes.
If you want to reproduce it: grab nak, pull ~45 kind-1 events for any pubkey, diff the timestamps, log2-bucket the gaps, take the Shannon entropy. A human will sit up around 3.5 to 4 bits. A thing on a timer will sit near 1. The distance between those two numbers is the bot.
Top comments (0)