| name | zframes |
| description | Build, update, serve, or fork the user's personal zframes market dashboard. Use when the user says "/zframes", "build me a dashboard", "set up my terminal", "make me a market terminal", "add X to my dashboard", "start/open/serve my dashboard", "fork this dashboard <url>", "run this shared zframes link", or wants a personalized live market dashboard (crypto + stocks). If they give a zframes explorer link (a .../d/<id> URL), fork it onto their machine. If a dashboard already exists and the user just wants to start it, serve it — don't rebuild from scratch. Writes a validated dashboard.json and serves it live with the CLI — the agent never writes React. |
zframes — your dashboard, generated
You set up and run the user's dashboard by driving the zframes CLI and
writing a dashboard.json spec. The CLI is the runtime: it serves a prebuilt
dashboard app pointed at that one file, editable in the browser. You only ever
write JSON. Never create or edit .tsx files for this task.
Three jobs, one skill — serve, update, or create. Decide which the request
is before anything else (step 1). "Start / open / serve my dashboard" just
serves the one the user already has — the most common ask once a dashboard
exists — and skips the interview and init entirely. Only the create path
runs the full build below.
0. The CLI
The runtime ships as the zframes CLI on npm. Always invoke it with
npx --yes zframes@latest <cmd> — npx fetches the published CLI (which bundles the
dashboard runtime) per run, so there's nothing to clone, install, or keep
current. The commands you'll use below are init, catalogue, lint, and
serve (written zframes <cmd> for brevity — always run them through npx).
1. Route the request — serve, update, or create
Classify the ask first; it decides everything below. The skill does three
jobs, and only one of them builds anything — never run the interview or init
unless you're genuinely creating a dashboard the user doesn't have yet.
- Serve / open / start an existing dashboard — "start my dashboard", "open
my terminal", "serve it", or a bare
/zframes when the user already has one.
This is the common case once a dashboard exists. Don't init, don't interview,
don't read the catalogue. Run zframes list to see what's there, then jump
straight to step 6 and serve: a bare zframes serve opens the default (the
* in the list), zframes serve <name> a specific one. That's the whole job —
if the store already holds the dashboard they mean, you're done after step 6.
- Update an existing dashboard — "add X", "swap the tickers", "change the
theme". Run
zframes list (the * marks the default) or find the
dashboard.json the user is serving / one in the current directory. Read it
first, then change only what they asked for — re-read the catalogue (step 2)
if you're adding frames, edit the frames array (step 4), then lint + serve
(steps 5–6). Don't re-init — you'd wipe their frames.
- Create a brand-new dashboard — only when the user wants one they don't yet
have (or explicitly asks for a fresh one alongside their others). Name it,
init it (below), then run the full build, steps 2 → 6.
- Fork a shared dashboard — the user gives a zframes explorer link
(
.../d/<id> or .../d/<id>/dashboard.json), or pastes the "fork" prompt.
They want that shared dashboard on their machine to keep and extend. Don't
interview or build — fetch it, land it in the store, serve, then offer to
personalize. See Fork a shared dashboard below.
The only artifact is a single dashboard.json. There is no app to scaffold —
the runtime comes from the CLI. Dashboards live in a global store
($XDG_CONFIG_HOME/zframes/dashboards, default ~/.config/zframes/dashboards),
so the user can run zframes from anywhere and keep several side by side, each
addressed by a short name (main, crypto, …). zframes list shows them
all, the * marking the one a bare zframes serve will open.
To create one, give it a name and init it into the store — don't
hand-write the envelope:
npx --yes zframes@latest init <name> --title "<dashboard title>" --author "<who>"
A bare token like crypto lands in the store as
<store>/dashboards/crypto/dashboard.json and becomes the default if you
don't have one yet (so a later bare zframes serve opens it; pass --default
to force it).
This writes a bare, already-valid dashboard — the fixed envelope, modelled on
package.json: version (semver string), title, author (pass --author if
the user gave a name, else it's left blank), then the 12-column grid
(geometry — columns/rowHeight/gap), the unicorn background, the theme
colours (accentHue/accentSat for the accent + baseHue/baseSat for the
dark card-surface tint + upColor/downColor for gain/loss), the typography
(fontFamily sans/mono/serif + numericStyle proportional/tabular + scale
global text size), and the card-surface appearance
(radius/borderStrength/surfaceOpacity/density/elevation) — with an
empty frames array. You never author that boilerplate or its geometry by
hand; you only fill in frames (step 4). That single file is everything the
user owns; sibling files it references (a daily-analysis.json brief, a local
image) live next to it in the dashboard's own dashboards/<name>/ folder, so
each dashboard's assets stay isolated. init refuses to clobber an existing
file unless you pass --force.
(Prefer a plain file over the store? Pass a path — init ./my-dir or
init ~/dash.json — and every command takes that path too. A token with a
/ or a .json suffix is always a path; a bare token is always a store name.)
Fork a shared dashboard (from an explorer link)
When the user gives you a zframes explorer link — a .../d/<id> URL, a
.../d/<id>/dashboard.json URL, or the pasted fork prompt — they want that shared
dashboard on their own machine to own and personalize. Don't interview or
build from scratch; fetch it, serve it, then offer to tweak it. This is the
web→local handoff: the explorer is the showroom; forking pulls the artifact home.
-
Fetch the spec. Resolve the raw URL: if the link already ends in
/dashboard.json, use it as-is; otherwise append /dashboard.json. Fetch it:
curl -fsSL "<url>/dashboard.json" -o /tmp/zframes-fork.json
(Or use your own web-fetch tool.) The result is a complete, valid
dashboard.json — the whole dashboard, not a fragment.
-
Land it in the store under a name. Pick a short name from the title, init
the store entry, then replace its file wholesale with the fetched spec:
npx --yes zframes@latest init <name> --title "<title from the spec>"
init prints the store path it created
(~/.config/zframes/dashboards/<name>/dashboard.json). Overwrite that file with
the contents of /tmp/zframes-fork.json — the fetched spec is the entire
dashboard, so replace, don't merge.
-
Lint + serve (steps 5–6): zframes lint <name>, then zframes serve <name>.
Now it's a real file they own, running live.
-
Offer to personalize. It's now an ordinary "update" (step 4 rules) — "want
me to swap in your tickers, add a frame, or retheme it?" Read the file, change
only what they ask, re-lint, and the page reloads.
2. Read the catalogue — always, before generating
npx --yes zframes@latest catalogue > /tmp/zframes-catalogue.json
Then read /tmp/zframes-catalogue.json with your file reader (it's ~25 KB of
JSON Schema). Redirect to a file and read the file rather than reading the
command's piped stdout — that guarantees the complete catalogue even if the
shell truncates large piped output. Frame names, config fields, and enum values
come from here — never from memory. The catalogue grows; your memory doesn't.
3. Interview the user (first run = onboarding)
The interview has one job: choose which tickers fill the dashboard. It never
decides which frames — every dashboard ships the full market frame set (step
4); the funnel only picks the symbols that populate them. Keep it to a short
three-step funnel, each step narrowing from the one before:
- Stocks, crypto, or both? The asset class. This is the only answer that
changes the frame set — it gates the asset-specific frames (US-stock frames
like
short-volume for stocks; bitcoin-dominance / tvl-treemap for
crypto; both → all of them). If they're vague, default to stocks.
- Which categories? Built from their step-1 answer, so they pick from groups
instead of recalling tickers cold. Offer a short multi-select menu (≤4 options
— pick the subset that fits what they said; don't dump the whole palette):
- Stocks → the
xyz dex is cross-asset, so the groups span asset
classes, not just equity sectors: equities Big Tech, Semiconductors &
Memory, AI & Data-Center, Crypto-adjacent, EV & Auto, Space & Defense,
Consumer & Health — plus Indices (S&P 500, Nasdaq-100), Commodities
(metals, energy), and FX (EUR/JPY/GBP). The xyz symbol reference below
maps every group to its valid tickers.
- Crypto → themes: Majors (BTC/ETH), Layer-1s, DeFi, Layer-2s, AI,
Memecoins.
- Both → offer both groupings.
- Any in particular? Within the chosen categories, ask them to name 3–5
specific tickers (free text — "NVDA, TSLA, AAPL"). Show and accept plain
tickers only; the
xyz: HIP-3 dex prefix is a framework internal you add
silently when writing the spec ("TSLA" → xyz:TSLA; crypto stays bare). If
they don't have specific names, you pick representative liquid tickers from
their chosen categories — don't send them off to research. Note which 1–2 are
the main focus; those drive the hero chart. If unsaid, take the first two.
That's the whole interview. Everything else — the full frame set, zones,
headings, the background, default config — is your call from the step-4 defaults;
never ask about it, and never read back frame or widget names as options.
This maps cleanly onto AskUserQuestion: step 1 is one question; step 2's options
are built from step-1's answer (a second round, not the same call); step 3 is the
free-text "Other" field. Label every option by asset / category / ticker,
never by frame name.
For "update my dashboard" requests, skip the funnel — read the existing
dashboard.json first and change only what they asked for.
xyz symbol reference (valid tickers by category)
The interview yields plain tickers; you write xyz:<TICKER>. xyz is the only
liquid HIP-3 dex — every symbol below lives there. Pick from this list so you
never emit a ticker the dex doesn't carry.
| Category | Liquid tickers — write xyz:… |
|---|
| Indices | XYZ100 (Nasdaq-100), SP500 (S&P 500) · thin: JP225 KR200 NIFTY DXY VIX |
| Big Tech | AAPL MSFT GOOGL AMZN META NVDA ORCL |
| Semiconductors & Memory | MU SKHX SNDK DRAM NVDA AMD AVGO ARM TSM MRVL QCOM · ETF SMH |
| AI & Data-Center | PLTR NBIS CRWV BE |
| Crypto-adjacent | MSTR COIN HOOD CRCL |
| EV & Auto | TSLA RIVN HYUNDAI |
| Space & Defense | SPCX (SpaceX) RKLB |
| Commodities | metals GOLD SILVER COPPER PLATINUM PALLADIUM · energy CL (WTI) BRENTOIL NATGAS · ETFs XLE URNM |
| FX | EUR JPY NOK GBP |
| Consumer & Health | NFLX COST DKNG GME HIMS LLY BABA BX |
Gotchas: it's xyz:SP500 (not SPY — that symbol doesn't exist on the dex),
xyz:XYZ100 for the Nasdaq-100 (not NDX/QQQ), xyz:CL for WTI crude (not
WTI/USOIL), xyz:SPCX for SpaceX. Crypto stays bare (BTC, ETH). Ignore
the other HIP-3 dexes (km/flx/vntl/…) — they're ~$0 volume; only xyz has
liquidity. The full live universe with 24h volumes is in docs/xyz-universe.html.
4. Fill in the frames
Edit the file init scaffolded (or the existing one for updates): add objects to
the frames array. Leave the envelope alone — version, grid,
background, theme, typography, and appearance are already set; only
touch them if the user explicitly asks (e.g. "more spacing" → bump grid.gap,
"square corners" → appearance.radius: 0, "muted accent" → lower
theme.accentSat, "warmer/blacker cards" → shift theme.baseHue / lower
theme.baseSat, "terminal look" → typography.fontFamily: "mono", "stop the
numbers jumping" → typography.numericStyle: "tabular", "bigger/smaller text" →
typography.scale, "colourblind / custom gain-loss colours" →
theme.upColor/theme.downColor, "glassy cards" → lower
appearance.surfaceOpacity, "no animation" → background.type: "gradient").
Show the full frame set — every dashboard gets all the market frames. You
don't cherry-pick frames by interest; build the whole comprehensive set and
populate the symbol-bearing frames with the user's tickers from the interview.
The only thing that varies the set is the asset class (step 1): the US-stock
frames for a stocks desk, the crypto frames for a crypto desk, all of them for
both. The pure-content frames (note, image, dino-game) stay opt-in — add
them only if the user asks; headings are structure (added per the zone rules
below). Start from this spine (with the interview's symbols), then add the rest
of the catalogue's market frames around it:
price-liveline hero — the user's 2–8 main symbols streaming together in
one live race. Keep normalize: true so stocks and crypto share an axis.
Place it big and up top: w: 8–12, h: 3.
- Four
price-chart cards on one row — each w: 3, h: 3 (3 × 4 = 12
columns, so all four sit side by side on the same row), all
"interval": "5m" (intraday), title = the ticker, and color-coded (a
distinct color hex per card). Split the rendering two and two: two
"mode": "candle", two "mode": "line". Default stocks picks when the user
named fewer than four — NVDA & TSLA as candles, AAPL & AMD as lines
(e.g. NVDA #76b900, TSLA #e82127, AAPL #0a84ff, AMD #f5a623). On a
crypto desk, use the user's coins instead (e.g. BTC & ETH as candles, SOL & a
fourth as lines).
short-volume (stocks desks only — FINRA reports US-equity short
volume) for the stock tickers ("sort": "shortPct"): w: 5, h: 4.
- The macro trio — always include all three. Keyless official-data context
every dashboard should carry; group them in a row under a "Macro" heading
(4 + 4 + 4 = 12 columns). None need config — schema defaults are sensible:
rates-board — NY Fed SOFR / effective fed funds / repo rates +
Treasury average rates. w: 4, h: 4.
yield-curve — the US Treasury par yield curve + the 2s10s spread.
w: 4, h: 3.
inflation-pulse — BLS CPI, month-over-month / year-over-year + trend.
w: 4, h: 3.
clock + market-hours — always include both. A clock set to the
market's timezone ("timezone": "America/New_York", "label": "New York"),
w: 3, h: 2; and market-hours for exchange open / closed status with next
open/close countdowns ("exchanges": ["NYSE","NASDAQ"] for a US-stocks desk,
or leave it empty for the world set), w: 4, h: 4.
- The rest of the catalogue's market frames — include them too, so the
dashboard reads as complete rather than sparse. From the catalogue you read in
step 2, add the remaining market frames that fit the asset class:
top-movers, price-ticker, and a normalized price-compare of the user's
tickers on any desk; fear-greed for market mood; and when crypto is in
scope, bitcoin-dominance, tvl-treemap, funding-rate-chart, and
funding-heatmap. Symbol-bearing frames take the user's tickers; context
frames need no config (schema defaults are sensible).
Layout rules for the frames:
- The grid is 12 columns,
rowHeight: 96 (set by init). Place each frame with
an explicit position: { x, y, w, h } in grid units.
- Group into zones. Put a
heading frame (full-width w: 12, h: 1) above
each group of related frames — e.g. "Markets", "On-chain", "Desk". Headings
render as bare section dividers (no card); they're what make a dashboard read
as designed instead of a widget dump. A good dashboard has 2–3 zones.
- Titles: usually omit. Every frame renders a polished default title from its
catalogue
label (e.g. "OI by Strike", "Funding Heatmap"), so leave "title"
unset by default — don't re-state the default and never abbreviate. Set a
per-instance "title" (sibling of frame/position) only when the label the
default can't know matters: required on every price-chart — use the ticker
("title": "TSLA", not "PRICE CHART") — and useful to distinguish otherwise
identical cards (e.g. which outlet a news-feed shows). Ignored by heading.
- Big charts:
w: 6–12, h: 3. Lists/tickers: w: 3–4, h: 3. Small cards
(fear-greed, bitcoin-dominance): w: 2–3, h: 3.
- No overlaps; no frame past column 12; every
id unique and human-readable.
- Only set config fields the user cares about — schema defaults cover the
rest, except required fields (the catalogue's
required list).
5. Lint — the feedback loop
npx --yes zframes@latest lint <name>
If it reports issues, fix the JSON and re-lint until clean. The error
messages name the frame instance and the exact field. Unknown frame names
come back with the list of valid ones — use it.
Renderer-level failures (a frame whose capability no provider covers) show
up as error cards in the running dashboard; treat those the same way.
6. Hand off — serve it
Serve the dashboard and open it for the user:
npx --yes zframes@latest serve <name>
serve hosts the prebuilt runtime pointed at that dashboard, streaming live
keyless data. A bare zframes serve (no name) opens the default store
dashboard, and when the store holds several the header title becomes a
dashboard chooser — the user opens it, picks another, and the page reloads
into it, no restart. The user can drag, resize, add, and configure frames in the
browser — Save writes the changes straight back to dashboard.json. Edits to
the file (yours or theirs) show on reload, so further "add X to my dashboard"
requests are just another edit + the page reloads. Pass --port <n> if 37263 is
taken.
Hard rules
- Serve when they just want to look. If a dashboard already exists and the
user says start / open / serve (or sends a bare
/zframes), serve it
(step 1 → step 6) — no interview, no init, no rebuild. Build or re-interview
only when they're creating a new dashboard or changing an existing one.
- The interview only picks tickers, never frames. Every dashboard ships the
full market frame set (step 4); the onboarding funnel — asset class →
categories → specific tickers — exists only to choose which symbols fill them.
Never ask which frames or widgets to include, never show or read back frame
names as options, and never make the user assemble the dashboard.
- dashboard.json is the only artifact. No React, no CSS, no new frames.
If the user wants a frame that doesn't exist, say so and list what does.
- Free data only: Hyperliquid (crypto + HIP-3 stock perps), DeFiLlama,
alternative.me, CoinGecko. There are no API keys to configure — never ask for one.
- Re-read the catalogue every session; never trust remembered frame names.