Arbitraging near-zero-cost inventory into the wal.sh sponsored surface
What sources exist, what they cost, what they leak, what they let you serve
Table of Contents
- 1. 1. What "cheapest possible" means here
- 2. 2. Source taxonomy
- 2.1. 2.1 RSS/Atom feeds — the load-bearing floor
- 2.2. 2.2 GitHub public API — very high rate at $0
- 2.3. 2.3 npm + PyPI new-releases
- 2.4. 2.4 arXiv / bioRxiv / OpenReview
- 2.5. 2.5 Wikipedia / Wikidata
- 2.6. 2.6 Public sitemap crawls
- 2.7. 2.7 Wire feeds — public sector
- 2.8. 2.8 Bandcamp / Kickstarter / IndieHackers
- 2.9. 2.9 Affiliate program catalogs
- 2.10. 2.10 Chumbox provider inventory (scrape)
- 3. 3. Per-source contract
- 4. 4. Decay + freshness
- 5. 5. Fitness scoring — which source serves when
- 6. 6. Attribution boundary
- 7. 7. Config surface
- 8. 8. Makefile
- 9. 9. Ledger integration
- 10. 10. What CAN'T be arbitraged
- 11. 11. Refutation
- 12. 12. Sequence — how this would build
- 13. Cross-references
1. 1. What "cheapest possible" means here
wal.sh's sponsored surface currently serves out of its own
/research/ corpus — inventory cost = $0 marginal (writing new
notes has an authorship cost but not an ad-acquisition cost). That
works but caps at 855 units and produces the pathologies documented
in the retrospective: closed
ecosystem, no counterparty, ACOS = ∞, 1.3% CTR that decomposes to 0
external clicks.
This document scopes the next arbitrage move: pulling third-party inventory into the same slot template, at zero or near-zero cost per unit acquired, and shipping it through the existing sponsored-research contract without inventing referents.
"Cheapest possible" carries two constraints:
- Zero dollars. No paid API tier, no chumbox-provider account, no affiliate-program signup that costs money. Free tier only.
- Zero authority claim. We do NOT tell the origin publisher we are serving them. We do NOT pay them. Every served unit points at the origin's canonical URL; the origin gets the click; we get nothing but observed CTR.
The result is a provably legal rebroadcast: reader sees a card, clicks through, lands on the origin. wal.sh gets no attribution credit and claims none — matches the retrospective §3.4 finding that provenance-at-write-time beats stats-at-audit-time.
2. 2. Source taxonomy
Ten sources examined. Four ship criteria per source: shape (what one unit looks like), rate (items/day at zero cost), refresh (poll interval that stays within the free-tier), decay (how fast a unit becomes stale enough to stop serving).
2.1. 2.1 RSS/Atom feeds — the load-bearing floor
Everyone with a blog publishes a feed. Most publish full-content feeds; the rest publish title + summary + link. Rate limits are per-request, not per-item.
Candidates:
- Hacker News front page (RSS via
hnrss.org) — 30 items, refresh every 5min at zero cost - Lobste.rs (
/rss) — ~50 items, similar cadence - Individual blogs (aggregated:
lobste.rsauthor list, our own webring, the Goodreads reading list — see the research brief) - arXiv new-listings per category —
/list/cs.PL/newetc., daily rollup, one XML per category
Per-unit shape after normalisation: :id :title :description :link
:pub-ts :source-feed:. Zero geo, zero attribution.
2.2. 2.2 GitHub public API — very high rate at $0
Unauthenticated GitHub API: 60 req/hour. Authenticated (personal token, no cost): 5,000 req/hour. Endpoints that give ad-shaped data:
/search/repositories?sort=stars&order=desc&created=>=2026-08-01— trending new repos/trending(undocumented but stable) — same/users/<u>/starred— reader's own stars for personalisation/orgs/<o>/repos— org-published catalogs (Anthropic, Cloudflare, Bytecode Alliance)
Rate: hundreds of items/hour at $0. Unit shape: :id :repo-name
:description :lang :stars :updated-ts :html-url:.
2.3. 2.3 npm + PyPI new-releases
Both expose new-releases feeds at zero cost:
- npm:
https://registry.npmjs.org/-/rss/updated(RSS) - PyPI:
https://pypi.org/rss/updates.xml
Volume is high (thousands/day). Freshness is minutes. Decay is weeks (a 30-day-old release is stale for adtech purposes). Filtering by tag/keyword narrows to relevance.
2.4. 2.4 arXiv / bioRxiv / OpenReview
Preprint feeds, per-category:
http://export.arxiv.org/api/query?search_query=cat:cs.PL&sortBy=submittedDate&sortOrder=descending- Free tier: 3-second rate limit, 3-day-cached; unlimited daily
- Per-unit:
:arxiv-id :title :abstract :authors :submitted-ts :category:
Ad-shape mismatch: abstracts are 200-300 words. Either truncate (loses the informational content) or serve as native long-form (defeats the "card" render). Best when a card is truly small (title + first sentence + "arXiv 2408.12345").
2.5. 2.5 Wikipedia / Wikidata
- Wikipedia:
MediaWiki API, unlimited-ish at $0 for read-only - Wikidata: SPARQL endpoint, generous free tier
- Unit shape:
:page-id :title :extract :thumbnail :link
Best fit: "sponsored-encyclopedic" — a query on wal.sh matches a Wikipedia article and serves it as a sponsored slot. Rarely overlaps with organic research, so the slot fills where the BM25 doesn't.
2.6. 2.6 Public sitemap crawls
Any site with a sitemap.xml exposes its URLs. Crawl the sitemap
of an adjacent publisher (Hacker News writeups, individual research
blogs, event sites like PLDI/POPL/ICFP) and use the URLs as
sponsored inventory.
Cost: bandwidth of the crawl (kbytes; $0). Legal: sitemap is
explicit invitation to crawl. Ethical: serving a competitor's URL
as "sponsored" ON wal.sh without their consent is the
[BROKEN LINK: No match for fuzzy expression: *3.4]
boundary case. Mark clearly: :source-kind "public-sitemap" in the
envelope so the auditor can flag.
2.7. 2.7 Wire feeds — public sector
Not the paid Reuters/AP wires. The zero-cost equivalents:
gov.ukJSON feed (all government publications, everything's Open Government Licence)- US federal press releases via FR.gov / whitehouse.gov feeds
- FDA drug-approval feed
- SEC EDGAR RSS (filings by CIK)
- NOAA severe-weather XML feeds
Each is $0, high-authority (real primary source), and rarely overlaps organic wal.sh content. Fits the "encyclopedic sponsor" niche.
2.8. 2.8 Bandcamp / Kickstarter / IndieHackers
Creator platforms with public new-releases feeds:
- Bandcamp: per-tag RSS
- Kickstarter: per-category RSS (recently-launched, staff-picks)
- Product Hunt: daily feed
- IndieHackers: recent-launches feed
Unit shape: :id :title :description :creator :thumb :link
:funding-goal :funding-current:. Interesting because the
"advertisement" IS the pitch; the creator wants the exposure.
Serving these is closer to genuine value-add than a chumbox tile.
2.9. 2.9 Affiliate program catalogs
The one paid-adjacent category. Programs like Amazon Associates, Skimlinks, Awin, Bookshop.org publish product catalogs to affiliates. Signup is free but the ToS constrains how you can serve the catalog.
Only source in the list that has a click-based revenue upside ($0.01-$0.10 per redirected purchase). Also the only source with a real ToS to respect (Amazon's is unusually strict — must display their disclosure, cannot mix with non-Amazon inventory without label separation).
Deferred as scope-heavy; deferred until the first eight sources are proven.
2.10. 2.10 Chumbox provider inventory (scrape)
The nuclear option. Taboola/Outbrain/Revcontent DOM-inject their inventory into third-party pages. Scrape the JSON payloads from their CDN endpoints and serve the units on wal.sh directly.
Do not do this. Every provider's ToS explicitly forbids third-party re-serving, the units carry unrelated-party affiliate parameters that route revenue to the original publisher, and the whole enterprise is exactly the kind of "inventory laundering" the retrospective's §3.4 stuffing detectors are designed to catch.
Listed here for completeness, and to name it as out of scope.
3. 3. Per-source contract
Every source produces units that MUST normalise to the
sponsored-research v-final unit shape (spec)
before entering the auction. Normalisation is per-source; the
contract downstream is shared.
R1. Each source ships a normaliser: (fetch, parse) → [Unit]. The
normaliser MUST NOT invent fields. If a source doesn't publish
:description, the unit's description is nil (not fabricated
prose).
R2. Every produced unit carries :source-kind ∈
#{"rss" "github-api" "npm" "pypi" "arxiv" "wikipedia"
"public-sitemap" "gov-wire" "creator-platform" "affiliate"}.
Downstream (auditor, ledger) reads this to classify.
R3. Every produced unit carries :origin-url — the canonical URL
on the source publisher. Every click routes there directly, no
redirect through wal.sh (except the necessary tracker fire).
R4. Every produced unit carries :fetched-at (ISO-8601). Decay
policy per source (§4) reads this.
R5. Per-source rate limiter: each fetcher records last-fetch-ts and refuses to fetch again inside the source's free-tier window. No "burst then throttle" — spread requests over the interval.
R6. Per-source cache: normalised units cached in
data/arbitrage/<source>/<date>.jsonl. Same one-line-per-day
pattern as data/event-flow/. Diffable, small, greppable.
4. 4. Decay + freshness
Not all inventory ages the same way. Serving stale inventory is worse than serving nothing.
| Source kind | Fresh window | Notes |
|---|---|---|
rss (HN, Lobsters) |
24h | anything older is off the front page |
github-api new repos |
7d | trending decays fast |
npm / pypi |
3d | new-version relevance fades |
arxiv |
30d | preprints stay relevant longer |
wikipedia |
365d | encyclopedic; ~no decay |
public-sitemap |
90d | site-dependent; conservative default |
gov-wire |
30d | topical; older filings served only on direct query |
creator-platform |
14d | funding windows expire |
R7. The auction MUST filter out units with (:fetched-at + decay
window) < now. Silent drop; not an error.
R8. Refresh cadence per source lives in
config/arbitrage-sources.edn (§7). Adjustable without redeploy.
5. 5. Fitness scoring — which source serves when
Not every source fits every query. The auction ranks sources per
query so a dinner query doesn't get an arXiv paper.
R9. Per-source affinity vector: fixed weights over the tracker's query shape. Sketch:
| Source | Cooking | Dev | Research | News | Local | Financial |
|---|---|---|---|---|---|---|
rss (HN) |
.1 | .8 | .5 | .3 | 0 | .1 |
github-api |
0 | 1.0 | .3 | .1 | 0 | 0 |
npm / pypi |
0 | 1.0 | .2 | 0 | 0 | 0 |
arxiv |
0 | .5 | 1.0 | 0 | 0 | .2 |
wikipedia |
.5 | .3 | .8 | .3 | .3 | .3 |
public-sitemap |
(site) | (site) | (site) | (site) | (site) | (site) |
gov-wire |
0 | .1 | .4 | .8 | .3 | .5 |
creator-platform |
.2 | .3 | .1 | .4 | .1 | .3 |
R10. Query classification via the tracker's existing pocket-es
tokeniser + a small keyword→category map (/site/static/query-categories.edn
— to author). Category vector × source-affinity vector →
per-source eligibility score. Top-scoring source wins the slot.
R11. Ties broken by freshness (newer fetched-at wins), never by random. Determinism is required for the auction proof to hold — same argument as retrospective §3.3 GSP over a single-bidder set.
6. 6. Attribution boundary
We serve without paying. That constrains what we can claim.
R12. Every served unit's tracker event MUST carry
:paid false — the ad system did not pay any counterparty to
acquire this inventory. This is the load-bearing separator from a
real retail-media network.
R13. Click-through routes DIRECTLY to :origin-url. wal.sh does
not intermediate; the origin publisher sees a normal referer from
https://wal.sh/. No cookie planting, no redirect, no affiliate
parameter appended.
R14. No revenue attribution table. There is no revenue. The
claimedCreditRatio (retrospective §3.4) is undefined; the ledger
records the impression + click but not a synthetic $ value.
R15. Take-down: any origin publisher can request removal of their
URL from the arbitrage index by adding a wal.sh-specific Disallow
line to their robots.txt:
User-agent: wal.sh-arbitrage
Disallow: /
The fetchers respect this exactly. Precedent: our own bot-compliance spec for how we expect to be treated when we're the crawler.
7. 7. Config surface
Single EDN file the operator edits. Every source has:
:enabled boolean, :refresh-secs, :decay-days, :affinity
map, :max-items-per-fetch.
{:sources
{:rss-hn {:enabled true :refresh-secs 300 :decay-days 1
:feed-url "https://hnrss.org/frontpage"
:affinity {:dev 0.8 :research 0.5 :news 0.3}}
:rss-lobsters {:enabled true :refresh-secs 300 :decay-days 1
:feed-url "https://lobste.rs/rss"
:affinity {:dev 0.9 :research 0.4}}
:github-trending {:enabled true :refresh-secs 3600 :decay-days 7
:api-endpoint "/search/repositories"
:params {:sort "stars" :order "desc" :q "created:>2026-08-11"}
:affinity {:dev 1.0 :research 0.3}}
:npm-updated {:enabled true :refresh-secs 1800 :decay-days 3
:feed-url "https://registry.npmjs.org/-/rss/updated"
:affinity {:dev 1.0}}
:pypi-updated {:enabled true :refresh-secs 1800 :decay-days 3
:feed-url "https://pypi.org/rss/updates.xml"
:affinity {:dev 1.0}}
:arxiv-cs-pl {:enabled true :refresh-secs 86400 :decay-days 30
:api-endpoint "http://export.arxiv.org/api/query"
:params {:search_query "cat:cs.PL" :sortBy "submittedDate"}
:affinity {:research 1.0 :dev 0.5}}
:wikipedia-any {:enabled true :refresh-secs 3600 :decay-days 365
:api-endpoint "https://en.wikipedia.org/w/api.php"
:affinity {:research 0.8 :news 0.3 :cooking 0.5 :local 0.3}}
:gov-uk {:enabled false :refresh-secs 3600 :decay-days 30
:feed-url "https://www.gov.uk/government/publications.atom"
:affinity {:news 0.8 :financial 0.5}}
:product-hunt {:enabled false :refresh-secs 3600 :decay-days 14
:feed-url "https://www.producthunt.com/feed"
:affinity {:dev 0.6 :creator 0.9}}}
:global
{:user-agent "wal.sh-arbitrage/1.0 (contact: j@wal.sh)"
:cache-dir "data/arbitrage/"
:respect-robots-txt true}}
8. 8. Makefile
Single per-note Makefile, tangled per §12, drives fetch + normalise
- deploy. Same pattern as
# arbitrage-inventory Makefile
# Lives in site/research/browser-adtech-infra/arbitrage-inventory-sources/
# Fetches, normalises, deploys arbitraged ad inventory into wal.sh.
CACHE_DIR := ../../../../data/arbitrage
CFG := config/arbitrage-sources.edn
BB := bb
.PHONY: help fetch-all fetch normalise validate corpus deploy audit clean
help: ## Show targets
@grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | \
awk 'BEGIN{FS=":.*## "}{printf " %-14s %s\n", $$1, $$2}'
fetch-all: ## Fetch every enabled source per its refresh cadence (respects rate limits)
@$(BB) scripts/fetch-all.clj --config $(CFG) --cache $(CACHE_DIR)
fetch: ## Fetch one source: gmake fetch SRC=rss-hn
@if [ -z "$$SRC" ]; then echo "Usage: gmake fetch SRC=<source-key>"; exit 1; fi
@$(BB) scripts/fetch-one.clj --config $(CFG) --source $$SRC --cache $(CACHE_DIR)
normalise: ## Normalise cached JSONL into the sponsored-research unit shape
@$(BB) scripts/normalise.clj --cache $(CACHE_DIR) --out $(CACHE_DIR)/normalised.jsonl
validate: normalise ## Run the sponsored-research contract validator on every normalised unit
@$(BB) scripts/validate-units.clj --in $(CACHE_DIR)/normalised.jsonl
corpus: validate ## Emit arbitrage-corpus.json (browser-side auction inventory)
@$(BB) scripts/emit-corpus.clj --in $(CACHE_DIR)/normalised.jsonl > \
../../../static/js/adtech/arbitrage-corpus.json
@printf "✓ arbitrage-corpus.json: %s units, %s bytes\n" \
"$$(jq '. | length' ../../../static/js/adtech/arbitrage-corpus.json)" \
"$$(wc -c < ../../../static/js/adtech/arbitrage-corpus.json)"
deploy: corpus ## Push corpus + config to prod
@rsync -avz ../../../static/js/adtech/arbitrage-corpus.json \
www_wal_sh@vps47738.dreamhostps.com:~/wal.sh/static/js/adtech/arbitrage-corpus.json
@echo "✓ arbitrage corpus deployed"
audit: ## Yesterday's arbitrage-serve breakdown by source-kind
@$(BB) ../../../../scripts/tracker-intent.clj \
--event sponsored.impression --format counts \
| grep -E "source-kind=|source_kind="
clean: ## Wipe cache (forces re-fetch on next run — respect rate limits!)
@rm -rf $(CACHE_DIR)
@echo "✓ cache cleared (rate-limit clock unchanged)"
9. 9. Ledger integration
Every fetch appends to the audit ledger. Every serve does not — the serve is already logged by the tracker.
R16. On successful fetch: append verdict=attributed subject=arbitrage-<source>-<date>
to .verify/chain.jsonl. Verifier arbitrage-fetcher. The
attributed verdict marks the units as sourced-not-verified —
we did not confirm the origin publisher meant to be indexed.
R17. On a take-down request (§6 R15): append verdict=corrected
with :FINDING naming the removed URL.
R18. Weekly rollup: a gmake arbitrage-week-audit (deferred)
would sum served-by-source and compare to fetched-by-source to
surface fetch-serve gaps (a source we're pulling but never serving
is waste; either boost its affinity or disable it).
10. 10. What CAN'T be arbitraged
Named boundaries. Adding a new source that violates one of these requires a spec bump.
- Paywalled content. Even if the paywall is bypassable via a cached-view trick, the served card MUST point at the paywall page, not the bypass. If the origin publisher can only monetise via subscription, we do not undercut them.
- Personal blogs without a public feed. A blog that publishes a URL but no RSS/Atom is not opting in to syndication; scraping is out.
- Content behind
robots.txt Disallow. R15 respect goes both ways. - Reader's own private data (Gmail, Google Drive, etc.). No.
- Chumbox provider inventory (§2.10). No.
- AI-generated content mirrors. If a source's feed is 100% LLM-shovelware, it's ad-shaped noise, not ad-shaped signal. Detection heuristic: >70% of the source's daily items have identical opening-3-tokens, or the source's URL structure follows a shovelware pattern. Silent drop.
11. 11. Refutation
The claim: arbitraged free-tier inventory can be served through the existing sponsored-research contract without inventing units, without paying the origin publisher, without misrepresenting the attribution, and at zero marginal cost per served card.
Fails if any of:
- CTR on arbitrage-served slots drops to 0% for a whole day (users ignore arbitraged content entirely — inventory quality gap)
- Any source produces >1 unit that fails the sponsored-research
validategate (contract leakage — normalisers are lying) - Any served unit's
:origin-urlresolves to a wal.sh URL (self-loop — arbitrage is now cannibalising organic; kill) - Any origin publisher requests removal AND we fail to honour it within 24h (R15 compliance breach — hard-fail; disable the source)
- The daily fetch cost (bandwidth + API calls) exceeds $0.01 cumulative across all sources (not "cheapest possible" anymore)
Test after 30 days of operation on a single-source config (start
with :rss-hn only): if any of the above trip, cut scope.
12. 12. Sequence — how this would build
- Author fetcher for
:rss-hnonly. Zero rate-limit surface, single feed, well-understood shape. Ship the whole pipeline end-to-end for one source before adding more. - Wire the sponsored-research contract's
unitconstructor to accept:source-kindas a valid extra field (§3 R2). Update validate. - Add
arbitrage-fetch-dailyto the daily cron chain (afterevent-flow-snapshot). - Serve
:rss-hnunits in one of the two sponsored slots on/search; grade after 7 days against §11 refutation. - If green: add
:lobsters, then:github-trending. - If red at 7 days: keep the doc, kill the pipeline. Ledger
the finding as
correctedagainst this spec. - Do NOT skip to §2.10 chumbox scraping; that's the failure mode this whole document exists to argue against.
13. Cross-references
- sponsored-research/spec.org — the contract every arbitrage unit ships through
- vegetarian-recipes-network/index.org — the opposite endpoint: closed-taxonomy, tightly curated inventory
- retrospective/index.org — §3.4 (provenance at write time), §3.8 (closed-ecosystem economics)
- attribution-audit/spec.org — the consumer of every served-arbitrage event
- bots/compliance-spec.org — how we crawl (mirrors R15 how we expect to be treated)
- event-flow-viz/index.org — the surface where arbitrage fill rate will show up