📅 Historical page. This content reflects minia2a as of its publication date and is kept for the record. Current model: x402 pay-per-call in USDC on Base only, 5 free trial calls per signed wallet, no credits and no top-up rail. → See current

A deterministic sort key that isn't: the tiebreak your deploy moves

November 15, 2026

Two copies of one page. Both 595,344 bytes. The same lines, in a different order. Our repo-vs-deploy parity check called that a difference for a week, and nobody had edited either copy.

The page is our blog index, regenerated every hour from the posts on disk. The generator's sort looked correct, and had been reviewed: primary key is the post's own declared date, newest first. The bug was the second key.

What the comparator looked like

// datePublished is 'YYYY-MM-DD', so a lexical compare is a date compare.
// Tiebreak on mtime so posts sharing a day (there are many) keep a deterministic order.
posts.sort((a, b) => {
  const d = String(b.datePublished || '').localeCompare(String(a.datePublished || ''));
  return d !== 0 ? d : b.mtime - a.mtime;
});

That comment is the whole bug. A tiebreak is only a tiebreak when it can distinguish the two items. Ask what happens when b.mtime - a.mtime is 0: the comparator says "these are equal", the sort leaves them where they were, and the effective third key is the order the array arrived in — here, directory traversal.

One rsync, one timestamp, to the nanosecond

Files written by a single bulk copy do not get "their own" mtimes. They get one write pass, and the pass has a timestamp. Measured on the host today:

$ stat -c "%y %n" public/blog/{edge-rewrites-your-bytes,hn-x402-thread-catalog-ledger,\
two-402s-brave-and-ours}-november-2026.html
2026-11-15 00:51:01.767017405 +0000  .../edge-rewrites-your-bytes-november-2026.html
2026-11-15 00:51:01.767017405 +0000  .../hn-x402-thread-catalog-ledger-november-2026.html
2026-11-15 00:51:01.767017405 +0000  .../two-402s-brave-and-ours-november-2026.html

Three posts, all dated 2026-11-13, and one mtime shared to the nanosecond. The tiebreak compared 0 to 0 for every pair, so the index's order for those three cards was whatever the filesystem returned first — which is not the same in a repository checkout as it is in a served webroot. Both copies were internally consistent. Neither was wrong about the dates. They just disagreed, every hour, forever.

How to check your own tree: find . -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n' | cut -d. -f1 | awk '{print $1" "$2}' | sort | uniq -c | sort -rn | head — a count above 1 is a group whose tiebreak on mtime is a no-op.

This is the 2026-11-05 bug, one day narrower

On 2026-11-05 we fixed a worse version of this: the index sorted on mtime alone, so correcting an old post re-pinned it to the top of the page while its card still printed its real date. Any rsync, any re-save, any deploy republished something old.

The fix put the declared date first and kept mtime as the same-day tiebreak, on the theory that same-day ties are rare and mtime makes them stable. Both halves of that theory are wrong in the same way: same-day ties are common (576 posts, many sharing a day), and mtime is exactly the metadata the deploy writes. We removed the disease from the primary key and left it in the secondary one, where it only fires on files that were copied together — which is most of them.

The fix

posts.sort((a, b) => {
  const d = String(b.datePublished || '').localeCompare(String(a.datePublished || ''));
  if (d !== 0) return d;
  return String(a.basename || '').localeCompare(String(b.basename || ''));
});

basename is the post's file name: unique by construction, part of the post set rather than of its delivery, and untouched by any deploy. Same-day posts now sort by slug. The output is a function of the posts and nothing else — the same 576 posts produce the same bytes on the host, in the checkout, and on a laptop.

The guard, including the part where the guard can be wrong

A claim that "the output depends only on the input" needs an experiment, not a code review. The check copies the same 619 post files into two scratch webroots — one in sorted order, one in reverse — which is what decides directory order on this filesystem, then runs the generator over each and requires one hash.

Both directions were measured:

generatortwo treesverdict
mtime tiebreaka7f0b9a0… vs e1ac8e7a…DIFFER (rc=1)
basename tiebreakone hashclean (rc=0)

The part worth copying: the mode reports CANNOT MEASURE (rc=2) when the two trees happen to traverse in the same order. In that case the comparison varied nothing, and calling it clean would be reporting success for an experiment nobody ran. An empty control is not a passing control.

What generalises

minia2a is a pay-per-call market for x402 endpoints — around 1,890 of them, priced in USDC on Base, five free trial calls per signed wallet. We publish this machinery because a market whose listings quietly drift is not a market, and the same reasoning applies to its index.

← All posts