Skip to content

Latest commit

 

History

History
165 lines (120 loc) · 10.7 KB

design-notes.md

File metadata and controls

165 lines (120 loc) · 10.7 KB

Crypto Prices Tidbyt — design notes

Compact design rationale, source choices, and trade-offs. The layout/flake/scripts mirror the sibling Longmont AQ Tidbyt project; this doc focuses on what's specific to a crypto ticker.

Data source: CoinGecko

CoinGecko's public REST API at api.coingecko.com is free for this kind of low-frequency reads and doesn't require registration. We use the /coins/markets endpoint because we need both 1-hour and 24-hour change — /simple/price only ships 24h:

GET /api/v3/coins/markets?vs_currency=usd&ids=cardano,midnight-3&price_change_percentage=1h,24h

Response (truncated, one entry per coin in a list):

[
  {
    "id": "cardano",
    "current_price": 0.1599,
    "price_change_percentage_1h_in_currency":  -0.49,
    "price_change_percentage_24h_in_currency": -1.41,
    "market_cap": 5948563346
  },
  {
    "id": "midnight-3",
    "current_price": 0.0332,
    "price_change_percentage_1h_in_currency":  0.61,
    "price_change_percentage_24h_in_currency": 3.72,
    "market_cap": 551057926
  }
]

The Pixlet fetch wrapper rekeys the list by id for data.get("cardano")-style lookup.

Why CoinGecko over alternatives:

  • Free, no API key needed for low cadence. Demo (registered free) tier raises rate caps if needed; Pro (paid) for production.
  • Aggregates across exchanges, smoothing out single-exchange weirdness.
  • One endpoint covers both coins in a single HTTP round-trip — the simple/price call accepts a comma-separated list of coin IDs.
  • include_24hr_change=true gives us the colored direction indicator for free.

Trade-offs:

  • 24h change is a rolling delta vs. ~24 h ago, not a UTC-midnight or local-midnight delta. Glanceable, less precise.
  • Free-tier endpoints occasionally return 429 (rate limit) under burst load. The 5-minute push cadence + ttl_seconds=300 HTTP cache keeps us comfortably under the cap. The app falls back to a PRICES N/A tile on non-200 responses; the next 5-minute cycle should recover.
  • USD-only. If you want EUR/JPY/BTC denomination, swap vs_currencies=usd for vs_currencies=usd,eur,… and adjust the format strings.

Coin identification — midnight-3 vs midnight

CoinGecko's search for "midnight" returns multiple coins, all symbol NIGHT. Two are listed:

CoinGecko ID Asset platform Market cap What it actually is
midnight-3 cardano (+ BSC) ~$552 M Cardano's Midnight privacy sidechain (Charles Hoskinson / IOG project). github.com/midnightntwrk, docs.midnight.network
midnight polygon-pos ~$35 K A Polygon memecoin named after a CEO's German Shepherd. Unrelated.

midnight-3 is the right ID. The deduplication was done by looking at asset_platform_id, the homepage URL, and the description — the Cardano one shows up unambiguously despite the symbol collision.

Display layout

The Tidbyt is 64 × 32 RGB pixels. Both coins live on a single static frame; each coin takes two rows for a total of four:

┌────────────────────────────┐
│ ADA              0.1597    │   row 1: symbol · price (white)
│ -0.49%           -1.41%    │   row 2: 1h change · day change (color-coded)
│ NIGHT            0.0332    │   row 3
│ +0.61%           +3.72%    │   row 4
└────────────────────────────┘

Four rows of tom-thumb (6 px tall) = 24 px content in a 32 px display. The remaining 8 px distribute asymmetrically: each coin's two rows are wrapped in an inner Column that packs them tight, and the outer Column uses main_align="space_around" to give a bigger between-coin gap (4 px) than the top/bottom edges (2 px each). Inside each coin pair the symbol/price and change rows sit flush. Visually that reads as two distinct two-line groups rather than four equally-spaced rows.

Per-row composition is Row(main_align="space_between") with two children:

  • Rows 1 & 3 — (symbol, price), both white.
  • Rows 2 & 4 — two signed percent strings, 1h on the left and 24h on the right. Each colored independently by its own direction (green up, red down, grey missing). So a coin that pumped in the last hour but is down on the day shows green on the left and red on the right — momentum context at a glance. Position implies timeframe (no 1h/24h or /h//d label) — earlier iterations with labels were too crowded on the NIGHT row.

Percent formatting

The percent values use magnitude-scaled decimals — 3 below 1%, 2 below 10%, 1 below 100%, 0 above — with +/- sign and % suffix. Trailing zeros are preserved. Starlark's % operator has no precision specifier, so the rounding is done by integer arithmetic (pick a factor for the magnitude band, scale, round, then format whole.frac manually).

Behavior across the range crypto actually produces:

Input Display
-0.4890 -0.489%
+0.671 +0.671%
-1.4107 -1.41%
+4.80 +4.80% (trailing zero preserved)
-12.345 -12.3%
+150.0 +150%
+1500 +1500%
+0.0001 +0.000% (rounds to zero in the magnitude band; sign + decimals retained for column alignment)
None —

Earlier iterations stripped trailing zeros (so +4.80 rendered as +4.8); the asymmetry between -1.41% and +4.8% in adjacent columns looked worse than the slightly-too-much precision at sub-1% values does.

Why no animation: an earlier version rotated through two single-coin frames (5 s each) with a larger 6x13 price font. The read time per coin was too short within Tidbyt's ~15 s app slot; you'd land in the middle of a frame, read part of the price, and the rotation moved on before you'd parsed it. The static 4-row layout sacrifices the big-font impact for the ergonomic win of always-visible data on both coins.

Precision: 4 decimals on the prices, 2 decimals on each percentage. For sub-dollar tokens like NIGHT (~$0.03) the 4-decimal price is only 3 significant figures, which is fine for at-a-glance awareness; a phone-screen view is better for precise trading decisions.

Label convention: no explicit 1h / 24h labels on the percent values — position alone (left = 1h, right = 24h) carries the meaning. Two earlier iterations (1h±X.XX% prefix and ±X.XX%/h suffix) were each too wide for tom-thumb on the NIGHT row; the labels were the easiest thing to cut once the percent formatter became 3-sigfig.

Color encoding:

State Color Hex
24h change ≥ 0 up green #00C800
24h change < 0 down red #FF3300
24h change missing neutral grey #888888
Symbol, price white #FFFFFF

The up/down greens and reds are deliberately a step less saturated than EPA red/green (used in the AQ project) so the same Tidbyt face isn't all-out neon. They still pass the room-distance glance test.

Caching and cadence

  • HTTP cache: http.get(url, ttl_seconds=300) — the in-process Pixlet cache caps fetches at one per 5 minutes per URL even if the daemon renders more often.
  • Push cadence: 5 minutes by default (PUSH_INTERVAL_S=300 in cryptoprices.yaml). Crypto prices move continuously but this is "ambient awareness," not trading data — minute-precision isn't needed.
  • CoinGecko free tier: ~30 calls/min. We use ~12 calls/hour. Headroom 150×.

Diagnostics

Every render emits a structured trace via Starlark print() — Pixlet routes these to stderr and the container loop captures both streams into podman logs. Per-render lines:

  • [fetch] GET <url> ttl=<seconds> key=<set|unset> — the CoinGecko request, with TTL and a non-leaking marker for whether a demo key was sent in the x-cg-demo-api-key header.
  • [fetch] CoinGecko HTTP=<status> bytes=<n> — response status and body size.
  • [fetch] CoinGecko coins returned=<n> ids=<list> — which coins came back; useful when a coin ID gets renamed upstream (the midnight-3 vs midnight lesson from this project) and the affected row goes blank.
  • [compute] <SYM> price=<usd> ch_1h=<pct> ch_24h=<pct> — one line per coin, with the values that feed the format helpers and color chooser.
  • [render] state=PRICES_NA (no coin data) — when both coins are missing.
  • [render] groups=<n> — final group count.

Replay: podman logs cryptoprices | grep -E '^\[(fetch|compute|render)\]', slice by the surrounding [<iso-ts>] push ok envelopes from the bash loop. API key is never logged (only "set" / "unset"), so it's safe to share log excerpts.

Starlark gotchas hit during development

  • No precision in % operator. Starlark's % operator supports the basic conversion characters (%s, %d, %f, etc.) but not width or precision, so "%.4f" % p errors out at runtime as unknown conversion %!.(MISSING). We format prices and percentages manually via str(p).split(".") instead. See _format_price and _format_change.
  • No while loop. Starlark intentionally omits while to keep programs total. Use for x in range(n) or, for string padding, "0" * n.

Open questions / stretch ideas

  • Sparkline. CoinGecko's simple/price doesn't ship one, but coins/markets?sparkline=true returns a 7-day price array. Could render a 2 × 32 px line beside each row.
  • More coins. Extending is one-line: append a CoinGecko ID to COIN_IDS and its display symbol to COIN_SYMBOLS. The row list auto-extends. Three coins fits at 32/3 ≈ 10 px per row, four at 8 px — past four the rows become hard to read.
  • Quote currency. If you want EUR or BTC denomination, swap vs_currencies=usd.
  • Market cap tile. A third frame could show 24h volume or market cap.
  • 24h-low/high markers. A small horizontal bar showing current price position within the 24h range.
  • Block-time aware fetch. Cardano-anchored tokens settle every ~20 seconds; the 5-minute cadence is way coarser than the underlying chain. Not actionable on a Tidbyt display, just noting.

File layout

File Purpose
main.star Pixlet app — single function fetches + renders the animation.
flake.nix Devshell + pixlet derivation + container image. Same shape as the AQ flake.
config-example.yaml Credential template (Tidbyt key/device/installation + optional CoinGecko demo key).
cryptoprices.yaml Podman kube spec for the push daemon.
scripts/*.sh check / preview / render / deploy / build-container / run-container.

Reference