Sell the directory as a corpus, on a four-hour clock - #163
Merged
Conversation
The directory stays open — every endpoint that answered a stranger yesterday still does, and llms.txt says so in as many words. What is new is a different artifact: the whole thing in bulk, which the open API was never shaped to serve and which this database cannot afford to give away. Four streams (feeds, items, extracts, authors) as gzipped NDJSON, cut on fixed four-hour UTC boundaries. The boundaries are the product, not the packaging: a window is the same set of rows whoever asks and whenever they ask, so a buyer walking them in order provably sees every row exactly once — no gaps from clock skew, no duplicates from a retry. Only closed windows are served, because the one containing the present is still filling. Streamed from a keyset cursor rather than built into a file: there is nowhere to put a file, and a periodic dump job would be a large read against a write-saturated database for an artifact most windows nobody collects. /sales carries no price and no rate card, only a contact form. Corpus licensing is negotiated per buyer, and a self-serve button would be selling terms nobody agreed to. Access is a hand-written row in dataset_grants after a conversation; there is deliberately no endpoint that mints one. Two things the page is careful about, because both would otherwise be assumed wrongly. Post records and article text are different datasets — content_html stopped being written in 0031, so the 4.7M figure is metadata and the prose is a smaller table — and publishers can now be excluded from the corpus while staying in the directory, which is a separate ask from removal on purpose. authors.email never leaves at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pij2tFcRgqceMpheotFSoX
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/salesand a licensed bulk-download surface for training-data buyers. The open directory is untouched: every endpoint that answered a stranger yesterday still does, andllms.txtnow says so explicitly.What's here
/sales— a public page with live figures read from the directory (no invented numbers, same cached read/advertiseand/crawlstatsuse) and a contact form. No prices, no rate card. Licensing is negotiated per buyer, so a published figure would be either wrong for everybody or a fiction with "contact us" under it. Enquiries land indataset_enquiriesbefore the notification email is attempted —sendEmailreports failure rather than throwing, and a sales enquiry lost to a Resend outage is the most expensive message this site can drop. Honeypot field plus a 3-per-hour-per-address cap.Four gzipped-NDJSON streams at
/api/dataset/{feeds,items,extracts,authors}, cut on fixed 4-hour UTC boundaries.The boundaries are the product rather than the packaging. A window is the same set of rows whoever asks and whenever they ask, so a buyer walking them in order provably sees every row exactly once — no gaps from clock skew, no duplicates from a retry, and a broken pull is simply repeated. Only closed windows are served; the one containing the present is still filling, and serving it would promise a completeness it does not have. An unaligned or future boundary is refused rather than rounded, because rounding mislabels the buyer's corpus in a way that only surfaces months later.
Streamed from a keyset cursor rather than built into a file: there is no object storage here, and a periodic dump job would be a large read against a write-saturated database producing an artifact most windows nobody collects. Cursors ride
(created_at, rowid)/(status, fetched_at, rowid)on indexes that already exist — a cursor on(created_at, id)would have needed a new index built over 4.7M rows at poller boot./api/dataset— an open manifest describing every stream, the cadence and the current boundary. Deliberately no row counts: it is unauthenticated, and acount(*)overfeed_itemswould hand anybody a way to make the site slow.Access is a hand-written row in
dataset_grantsafter a conversation. There is no endpoint anywhere that mints one, and that is the intended amount of friction. A session or an API key both work; the licence hangs off the account, so a buyer can rotate keys without telling us. Refusals are distinguishable — 401 (sign in) / 401 (bad credential) / 402 (no licence, here is /sales) — because they need three different actions.Two things worth reviewing closely
Post records ≠ article text, and the page says so unprompted.
content_htmlstopped being written in0031_item_body_on_demand.sql, so the 4.7M figure is metadata and the prose lives initem_extracts(~275k articles, ~7.8k chars each, growing with reader attention rather than with the crawl). A buyer who learns this after signing is a refund; the sales page and the manifest both lead with it.A publisher can now opt out of the corpus while staying in the directory.
feeds.dataset_opt_out, honoured by all four streams — includingextracts, which reaches the flag two joins away, and there is a test for exactly that. Today the only way to express "list my blog, but don't sell my writing to a model" is to ask for removal, and the directory loses a good feed over a question it never asked.authors.emailis never emitted. The column holds addresses people published as their own contact; a corpus of contactable humans is a different product from a corpus of what they wrote.Verification
packages/dbtests, 11 newapps/webtests. Full suite 1,325 passing on Node 22 (matching CI),pnpm buildclean.author_links.kind, which does not exist (it isnetwork). An empty fixture table cannot fail a query. Fixed, and the test that would have caught it is now in the suite — the links now also carryhandle,sourceand theverifiedrel="me" flag.Deploying
The migration applies itself at poller boot.
alter table feeds add columnwith a constant default is a schema-only change in modern SQLite, so it does not rewrite 535k rows. Nothing else is needed.🤖 Generated with Claude Code
https://claude.ai/code/session_01Pij2tFcRgqceMpheotFSoX