●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Building Dashboards with Claude Artifacts — Why I Aggregate Before Pasting, and the Setup That Stuck
Claude artifacts render as React components. Working from that fact, here is how I stopped pasting raw CSVs and settled on a setup that separates preprocessing, data contracts, and refresh paths.
At the end of each month I open AdMob, App Store Connect, and Firebase in turn and export the CSVs. Running several wallpaper apps as an indie developer, I would rather decide where the next update goes by looking at numbers than by going on instinct.
For a long time I pasted those CSVs straight into Claude. A few seconds later, a plausible line chart appeared. It felt good.
What tripped me up was noticing that the weekly peak in the chart sat one day away from the peak in my own spreadsheet.
The cause turned out to be almost disappointingly simple. The data I pasted was never read all the way through.
An artifact renders as a React component. Whatever data you hand over has to get transcribed into an array literal inside that code at some point. Pasting 4,312 rows of CSV is, in effect, asking for 4,312 objects to be typed out one by one. That is not what happens.
What follows is the setup I arrived at once I accepted that constraint — the path from raw exports to numbers I can actually make decisions with, along with the code I run today.
The CSV You Pasted Did Not Arrive Intact
I started by confirming what was happening, with numbers.
The 90-day daily report exported from AdMob was 4,312 rows and 178 KB. I pasted it, opened the source of the generated artifact, and counted the elements in the data array. There were 117.
Nothing announced this. There was no note saying "I sampled representative values" — just a line chart, drawn without comment. The weekly peak sat a day off because, after the thinning, an adjacent day happened to look like the high point.
I do not think of this as a defect. Output tokens are finite, and transcribing 4,312 rows would consume most of a response on its own. Fitting the data to what can reasonably be written is a sensible call.
The problem was that I never asked for that call to be made.
How to Check
When you suspect the same thing, opening the artifact's code is the fastest route.
Open the artifact's code view
Find the data array declaration (the equivalent of const data = [...])
Count the elements and compare against your source row count
If they diverge, do not use that chart for decisions
A gap of three rows is rounding. A gap of four thousand is a different dataset. Since I started making this check, I have stopped second-guessing my charts.
Do the Aggregation Yourself — 178 KB Down to 3.1 KB
The approach I settled on was to stop handing the aggregation to Claude at all.
Of those 4,312 daily rows, the only thing that drives a decision is the weekly trend per app. So I fold the data into that shape before it goes anywhere. Here is the script I actually use. It reads the AdMob daily export and emits weekly JSON.
# aggregate_admob.py — fold an AdMob daily CSV into a weekly summary JSON# usage: python3 aggregate_admob.py admob_daily.csv > weekly.jsonimport sysimport jsonimport pandas as pdREVENUE = "Estimated earnings (USD)"IMPRESSIONS = "Impressions"def main(path: str) -> None: df = pd.read_csv(path) df["Date"] = pd.to_datetime(df["Date"]) # Pin weeks to a Monday start. Skip this and your totals land # one day off from the console's own weeks (mine did). df["week"] = df["Date"].dt.to_period("W-SUN").dt.start_time grouped = ( df.groupby(["App", "week"]) .agg(revenue=(REVENUE, "sum"), impressions=(IMPRESSIONS, "sum")) .reset_index() ) # eCPM is total revenue / total impressions * 1000. # Averaging daily eCPM instead inflates the number, because # low-impression days carry the same weight as busy ones. grouped["ecpm"] = (grouped["revenue"] / grouped["impressions"] * 1000).round(2) grouped["revenue"] = grouped["revenue"].round(2) grouped["week"] = grouped["week"].dt.strftime("%Y-%m-%d") records = grouped.to_dict(orient="records") print(json.dumps(records, ensure_ascii=False, separators=(",", ":")))if __name__ == "__main__": main(sys.argv[1])
Here is what I measured.
Metric
Pasting the raw CSV
Pasting the weekly fold
Payload
178 KB / 4,312 rows
3.1 KB / 26 rows
Input tokens (approx.)
~47,000
~900
Elements in the artifact
117 (thinned)
26 (complete)
Agreement with the numbers
Weekly peak off by one day
Matches the spreadsheet
Cost of regenerating
Re-paste everything each time
Swap the JSON
That is roughly a 98% token reduction, but the tokens were never the point. What mattered was that every row now arrives, and that rebuilding got cheap.
The W-SUN line is the bug itself, written down. Pandas defaults its weekly periods to a Sunday close, which lands a day away from how the ad console groups its weeks. Pinning it made the original one-day gap disappear.
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A working pandas preprocessing script that takes a 178 KB raw CSV down to 3.1 KB — a measured ~98% token reduction
✦A cron + static JSON setup that delivers a daily-refreshing dashboard despite artifacts having no external fetch and no localStorage
✦A 4-step data contract that stopped keys and axes from drifting on every regeneration
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Even after handing over folded JSON, key names and axis assignments drifted between regenerations. revenue would come back as earnings. The Y axis would split in two, then collapse back to one.
The fix is unglamorous: state the contract up front.
Fix the keys and types each array element carries, in plain prose
Spell out the axis assignments (left and right)
Add one line: do not add or rename keys
When something needs to change, edit the contract before asking
The declaration I actually paste is this short.
Data contract (please do not deviate from this shape): week: string "YYYY-MM-DD" (Monday start) app: string revenue: number (USD, 2 decimals) impressions: number (integer) ecpm: number (USD, 2 decimals)Rendering: left axis = revenue (bars) right axis = ecpm (line) one series per app do not add or rename keys
Once the contract is in place, the requests get short. "Switch the right axis to ARPDAU" now lands on its own, without re-explaining the whole picture each time.
The Artifact Code I Actually Run
Below is the React component I settled on, written against that contract. It is a Recharts composed chart: revenue as bars, eCPM as a line. It runs inside an artifact as-is.
import { useMemo, useState } from "react";import { ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, ReferenceLine,} from "recharts";// Paste the JSON your preprocessing script emitted right hereconst RAW = [ { week: "2026-01-05", app: "Wallpapers", revenue: 412.8, impressions: 318400, ecpm: 1.3 }, { week: "2026-01-12", app: "Wallpapers", revenue: 455.1, impressions: 331200, ecpm: 1.37 }, { week: "2026-01-19", app: "Wallpapers", revenue: 398.4, impressions: 349800, ecpm: 1.14 }, { week: "2026-01-26", app: "Wallpapers", revenue: 501.6, impressions: 356100, ecpm: 1.41 },];// A colorblind-safe pairing. Left to its own devices, the default// palette puts red and green next to each other.const PALETTE = { revenue: "#0072B2", ecpm: "#E69F00", target: "#999999" };const ECPM_TARGET = 1.3;export default function AdRevenueDashboard() { const [showTarget, setShowTarget] = useState(true); const { data, delta } = useMemo(() => { const sorted = [...RAW].sort((a, b) => a.week.localeCompare(b.week)); const head = sorted[0]?.revenue ?? 0; const tail = sorted[sorted.length - 1]?.revenue ?? 0; // Guard the divide here, or a month whose first week is empty // renders NaN in the header. const pct = head === 0 ? null : ((tail - head) / head) * 100; return { data: sorted, delta: pct }; }, []); return ( <div className="w-full p-4 space-y-3"> <div className="flex items-baseline justify-between"> <h2 className="text-lg font-semibold">Weekly revenue and eCPM</h2> <span className="text-sm text-gray-600"> {delta === null ? "No opening data" : `${delta.toFixed(1)}% vs. opening week`} </span> </div> <label className="flex items-center gap-2 text-sm"> <input type="checkbox" checked={showTarget} onChange={(e) => setShowTarget(e.target.checked)} /> Show the eCPM target line </label> <ResponsiveContainer width="100%" height={320}> <ComposedChart data={data} margin={{ top: 8, right: 16, bottom: 8, left: 0 }}> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="week" tick={{ fontSize: 12 }} /> <YAxis yAxisId="left" tick={{ fontSize: 12 }} /> <YAxis yAxisId="right" orientation="right" tick={{ fontSize: 12 }} /> <Tooltip /> <Legend /> <Bar yAxisId="left" dataKey="revenue" name="Revenue (USD)" fill={PALETTE.revenue} /> <Line yAxisId="right" type="monotone" dataKey="ecpm" name="eCPM (USD)" stroke={PALETTE.ecpm} strokeWidth={2} dot={{ r: 3 }} /> {showTarget && ( <ReferenceLine yAxisId="right" y={ECPM_TARGET} stroke={PALETTE.target} strokeDasharray="4 4" label={{ value: `Target ${ECPM_TARGET}`, position: "right", fontSize: 11 }} /> )} </ComposedChart> </ResponsiveContainer> </div> );}
Swapping the contents of RAW is all next month costs. Not having to ask for a rebuild turned out to matter more than I expected.
The zero guard inside useMemo comes from having shipped a NaN to my own screen. The month I added a new app, its opening week was empty and the comparison broke. The edges of real data are always dirtier than you plan for.
Keep "Real-Time" Outside the Artifact
This is the part worth stating plainly.
An artifact's sandbox cannot fetch external URLs. It cannot use localStorage either. A dashboard that polls an API every 30 seconds does not exist as an artifact alone.
Not knowing that, I once spent a while hunting for the bug in a polling dashboard that had no bug. The mistake was the venue, not the code.
So I moved the refresh responsibility outside.
Let Claude Code handle fetching and aggregation, emitting weekly.json
Drop that JSON alongside the static site
Let a self-hosted HTML page do the rendering, not the artifact
Run it daily on cron
The artifact is where the shape gets decided. The self-hosted page is where I look every day. Drawing that line made both behave.
#!/usr/bin/env bash# daily_dashboard.sh — aggregate daily and swap the dashboard JSONset -euo pipefailWORK="$HOME/metrics"OUT="$WORK/public/data/weekly.json"cd "$WORK"# Fold the exported CSV. On failure, leave the existing JSON alone —# yesterday's numbers beat a truncated file.if python3 aggregate_admob.py exports/admob_daily.csv > /tmp/weekly.json.new; then mv /tmp/weekly.json.new "$OUT" echo "[$(date +%F)] updated: $(wc -c < "$OUT") bytes"else echo "[$(date +%F)] aggregation failed, keeping previous JSON" >&2 exit 1fi
Refusing to overwrite on failure is there because I did the opposite once. An empty JSON landed on top of a good one, and the next morning the dashboard was blank. Stale and correct beats fresh and broken — that is the tradeoff I recommend in production.
There is one more correction I make every single time.
Leave the series colors unspecified and you tend to get red against green. For anything showing gain and loss it is an intuitive pick, and for readers with protanopia or deuteranopia it is a hard one to read.
I default to blue (#0072B2) and orange (#E69F00). That is why PALETTE sits in a constant in the code above. Putting the colors in one place beats asking for them in prose each time.
Vary the line style too, and the chart survives printing and screenshots. Solid for measured values, dashed for targets. Those two rules alone remove the dependence on color.
What Broke, and What Fixed It
Symptom
Actual cause
Fix
Chart peak sits a day off from the spreadsheet
Week boundaries default to a Sunday close
Pin W-SUN in preprocessing
Fewer data points than expected
Transcription cut short by output token limits
Open the artifact code, count elements, aggregate first
Polling never fires
The sandbox cannot fetch external URLs
Move refresh out to cron + static JSON
Opening-week comparison shows NaN
A new app has an empty first week
Guard the divide explicitly and return null
Key names change on every rebuild
No data contract was declared
Fix keys, types, and axes up front
eCPM reads higher than it feels
Daily eCPM values are being averaged
Compute total revenue / total impressions
Dashboard blank in the morning
A failed run overwrote the JSON with nothing
Keep the previous file and exit non-zero
Four Rules I Hold To
Six months in, this is all that is left.
1. Count before trusting. Reconcile element counts against the source before a chart informs anything. The check takes ten seconds.
2. Aggregate on your own machine. Hand over the fold, not the raw file. It is also a way of keeping the judgment yourself.
3. Put the contract first. Declare keys, types, and axes before asking. Conversations get shorter and the drift stops.
4. Move refresh outside. The artifact decides the shape; the self-hosted page is the daily view. Keep the roles apart.
There is one thing I gave up, too: asking for "a nice dashboard." What comes back is always attractive, and always different. A tool you look at every morning has no use for a new shape every morning.
Pick one CSV you already have and paste it into an artifact. Then, before you look at the chart, open the code view and count the elements in the array.
If the count matches your source rows, use it. If it does not, adding a single preprocessing script is where this begins.
For a stretch there, I believed I was looking at my numbers when I was looking at an impression of them. Learning what a tool is made of is not the same as distrusting it. It is redrawing the line between what you delegate and what you keep — and that line is worth redrawing now and then.
Thank you for reading. I hope some of this proves useful in your own setup.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.