Data first, then take: the day FRED actually moved through the pipeline
Last week's post was about the Bridge — the local Node server that lets my workflows call Claude. This week the Bridge carried something more interesting than a blog post: a 30-day chart of the 10-year U.S. Treasury yield, fetched from FRED, written to my database, and rendered through a free public chart service. Two days ago the number was 4.35. As of Thursday's close it was 4.47.
That twelve-basis-point move is not the point of this post. The fact that I had to look it up before writing this sentence is the point of this post.
I have a finance site, distillfin. For most of its existence — six months — it ran on the exact opposite operating model. A trending topic would surface (usually some macro narrative recirculated from the prior week), a Claude prompt would generate a 2,000-word "explainer," and a chart was either omitted entirely or pasted in as decoration after the take was already written. The take came first. The data, if it appeared at all, came second. That ordering is what produced the 177 posts I had to noindex when I admitted the site was a YMYL liability rather than an asset.
The rebuild this week reverses that. I'm calling this post Ravi's side, because the persona running distillfin now — Ravi Krishnan, the data writer I introduced two weeks ago — has one rule baked into his voice spec at the prompt level: every macro claim must reference a specific named series or filing. No "studies show." No "analysts expect." No "history suggests." A named FRED series, or a named SEC filing, or no claim. That's it.
This post is about what it took to make that rule mechanically enforceable, and what surprised me about doing it.
What I built, in three endpoints
The Claude Bridge already had a couple of dozen endpoints — file I/O, FFmpeg, Postiz uploads, Firecrawl search, Claude itself. This week I added three more:
POST /fred-fetch { "series_id": "DGS10", "limit": 30 }
POST /log-event { "event_type": "caffeine", "numeric_value": 200, "unit": "mg" }
GET /events-chart?type=caffeine&days=30
The first one is for Ravi. The second and third are for Daniel — I'll get to him next week. Today is about /fred-fetch.
It does exactly what it says. n8n posts a JSON body with a FRED series ID and a desired observation count. The Bridge calls FRED's free API, parses the observations (filtering out the dot-placeholder rows FRED uses for missing data), upserts the rows into a Supabase table called market_snapshots, and builds a QuickChart URL that renders the series as a Chart.js line chart on demand. The response back to n8n is a small JSON blob with the first observation, the last observation, the count, and the chart URL.
The whole thing is about 70 lines of Node. I'm noting that because the boring number — 70 — is the answer to a question I'd been quietly avoiding for months. Six months ago, when I was generating distillfin posts that included no real data, I told myself the reason was that "real data integration is a real project." It is not a real project. It is one afternoon of careful work, and the reason it had felt big was that it was sitting downstream of a more uncomfortable admission, which was that without real data I had nothing distinctive to write.
What FRED actually gives you
For anyone who hasn't used it: FRED is the Federal Reserve Bank of St. Louis's economic data service. Free API key. 800,000+ time series ranging from headline indicators everyone knows — DGS10, CPIAUCSL, UNRATE — down to obscure aggregates like the Atlanta Fed's wage growth tracker for prime-age workers in the third quartile of educational attainment. (Yes, that exists. It's FRBATLWGT3Q1525YQ or something similar.)
The API returns JSON. Observations look like this:
{
"date": "2026-05-14",
"value": "4.47",
"realtime_start": "2026-05-15",
"realtime_end": "2026-05-15"
}
Three things worth knowing if you're going to write about this stuff:
Values are strings, not numbers. Including the string "." for "this observation is missing." If you don't filter that out before plotting, your chart will render with a giant zero where reality has a small gap. Easy to miss, easy to ship.
realtime_start is not the observation date. It's when that observation was published or revised. For series that get revised (which is most of them), the distinction matters. The first time GDP for a given quarter is reported, then revised a month later, then revised again at the annual benchmark, FRED tracks all three vintages. If you want a clean recent-history chart, sort by date and ignore realtime_*. If you want to write about how forecasts evolve, the realtime fields are everything.
The free key is generous. I'm running maybe a hundred calls a week and FRED hasn't flinched. There's no per-call charge, no enterprise tier I had to wave off, no friction. This isn't a sponsored mention; it's a public-good institution maintained at taxpayer expense, and using it well is part of what makes data writing possible at all.
The first chart
I ran /fred-fetch on DGS10 (10-year constant-maturity Treasury) with a 30-observation window two days ago. The chart that came back showed:
- First observation: 4.35, April 3
- Last observation: 4.47, May 14
- Shape: noisy drift slightly upward; one dip to 4.26 mid-April; back above 4.40 by end of month
That's it. That's the entire chart. There is no "story" in twelve basis points of drift over forty days. If you write a 2,000-word "what the move means" piece off this chart, you are inventing a story to fit the noise, and any halfway alert reader will know it.
This is the awkward part of "data first, then take." Most days, the take is there isn't one yet. Or the take is so narrow it doesn't sustain a post by itself. A serious finance writer can sit with that. A content-mill writer cannot, because the mill demands volume. The mill's incentive is to find a story in every chart. Ravi's incentive — the persona, the rules, the byline — is to find the chart that contains a story, and to leave the other charts alone.
In practice this means distillfin under the new model publishes less often, and the posts that do publish are shorter on take and longer on chart. The honest answer to "what does DGS10 say this week?" is usually "drift, in the range it's been in for two months, nothing has resolved." That sentence is 18 words long. It is not the foundation of a blog post. It is the foundation of not writing a blog post this week, which used to be unthinkable when the cron was firing every two hours and the volume was the point.
What gets harder under the new rule
Two specific things, and both surprised me.
Finding the right series is harder than reading it. DGS10 is famous. Most series are not. If I want to write about how mortgage rates are behaving relative to the 10-year — a perfectly reasonable thing to want to look at — I need MORTGAGE30US, and I need to know that's what it's called, and I need to know that it's published weekly on Thursdays not daily. The first time I built this kind of chart I spent more time finding the series name than writing the analysis. That ratio doesn't really go down with practice; it just shifts to longer-tail series.
The reader's expected take goes up, not down, when the data is real. Counterintuitive. When I was generating "10 things to know about Treasury yields" posts with no charts, the bar was low and the audience knew it. The moment a real chart shows up in the post, the implicit promise becomes: this writer knows enough about this chart to tell me something I can't see for myself. If I then deliver a generic recap, the post is worse than the no-chart version, because I've raised the bar and then walked under it. Data first is also commitment first.
I think this is the part of the rebuild I'd most want to warn anyone else away from underestimating. Adding charts to a site doesn't make the site good. Adding charts makes the site visibly accountable, which is different. If you're not ready to write as if a smart reader is checking your math, charts will hurt you faster than they help.
What's next
The endpoint is live. The first DGS10 chart is sitting in market_snapshots already. The Ravi voice spec is wired into the Phase 8a prompt. The first auto-generated distillfin draft under this regime fires on Tuesday, and I read it before it ships — same as everything else now. We'll see whether the persona actually pulls a real series and writes a real-data sentence, or whether it punts and writes a generic macro paragraph anyway, in which case the prompt needs another iteration.
Next week I'm writing about Daniel — the spanpost persona — and the /log-event endpoint. The setup is the same shape as Ravi's: I built the rail, the persona has a rule that says "use it," and we find out whether the rule survives generation pressure. The output category is different (caffeine-and-focus logs instead of macro charts) but the failure mode is identical: if the rail doesn't get used, the post collapses back into the slop we started with.
The longer arc here, three posts in, is starting to take a shape I didn't fully see when I wrote R1. The rebuild isn't really about personas, and it isn't really about charts. It's about whether the system can be configured to make the lazy path harder than the honest path. Six months ago, the lazy path was easy and the honest path required a project plan. The whole point of this month's work has been to invert that — to make "write a serious post backed by a real series" the path of least resistance, and "write a generic explainer" the path that fails Quality Gate. Whether I've actually inverted it, or only moved a few obstacles around, the next four Tuesdays will tell.
— Jay
