[DISCUSS] The “Never 0.00” Challenge — design a resilient price pipeline (client → edge) together

Leader posted 3 min read

[DISCUSS] The “Never 0.00” Challenge — design a resilient price pipeline (client → edge) together

New group, first thread. Let’s start with a problem every finance app faces: your quotes flicker to 0.00 right when users need truth. How do we design a pipeline that never panics, stays fast, and makes the UI explainable?

I’m 22s from Pocket Portfolio (OSS dashboard: CSV → live P/L → Mock-Trade Lab). We’ve been hardening our price path and want to compare notes with you — architects, frontenders, edge-folk, crypto quants, and a11y pros.


The story (2 mins)

You’re rendering a positions grid. Provider “A” hiccups for 900 ms, “B” is slow in one region, and “C” returns a weird NaN. Your UI shows 0.00 or a spinner graveyard. Users lose trust.

We flipped the rule: it’s better to show a clearly tagged stale-but-true quote than a shiny 0.00. The UX stays calm, and the system buys enough time to recover.


The pattern (client → edge)

Client
  └── fetch quote(sym) with 1.4s budget
      └── Edge function (region-local)
          ├─ Try P1 (timeout 1.4s) -> OK? return
          ├─ Try P2 (1.4s) -> OK? return
          ├─ Try P3 (1.6s) -> OK? return
          └─ else: return last-known-good (stale:true) + tiny error summary
Cache
  └── KV: sym -> { price, ts, src }  // updated on successful hits

Key properties:

  • Short, brutal timeouts per provider (fail fast).
  • Rotation and backoff to avoid “thundering herd”.
  • Last-known-good as final fallback (with a stale flag).
  • Zero logs spam — count errors, sample detail, keep it cheap.

Tiny TypeScript example (edge)

// /edge/quote.ts (simplified)
const providers = [
  { name: 'p1', url: env.P1_URL, timeoutMs: 1400 },
  { name: 'p2', url: env.P2_URL, timeoutMs: 1400 },
  { name: 'p3', url: env.P3_URL, timeoutMs: 1600 },
];

export async function getQuote(sym: string) {
  for (const p of providers) {
    try {
      const ctl = new AbortController();
      const t = setTimeout(() => ctl.abort(), p.timeoutMs);
      const r = await fetch(`${p.url}/q?symbol=${sym}`, { signal: ctl.signal, cf:{ cacheTtl: 10 }});
      clearTimeout(t);
      if (!r.ok) continue;
      const { price } = await r.json();
      if (Number.isFinite(price)) {
        await KV.put(`q:${sym}`, JSON.stringify({ price, ts: Date.now(), src: p.name }), { expirationTtl: 120 });
        return { price, src: p.name, stale: false };
      }
    } catch {}
  }
  const last = await KV.get(`q:${sym}`);
  const cached = last ? JSON.parse(last) : null;
  return { price: cached?.price ?? null, src: cached?.src ?? 'none', stale: true };
}

UX that earns trust (copy/paste checklist)

  • Never show 0.00 unless a symbol truly trades at zero (rare).
  • ✅ If stale, show a subtle chip: Last update 28s • p2 with tooltip “Provider fallback active”.
  • Keyboard-first: when a value changes, announce to SRs (aria-live="polite") and use diff-highlights that fade in <1s.
  • Explain the why: on hover, show “Switched p1→p2 (timeout)”.
  • Error CSV for failed imports; same spirit: explain, don’t mystify.

Mini-spec template (fill in and post as a reply)

policy:
  ttl_cache_seconds: 10
  providers:
    - name: p1
      timeout_ms: 1400
    - name: p2
      timeout_ms: 1400
    - name: p3
      timeout_ms: 1600
  final_fallback: last_known_good
  stale_badge_after_ms: 15000
  count_telemetry: true   # no PII, just counters
ui:
  show_stale_chip: true
  announce_changes: polite
  hover_explain: provider_switch
observability:
  per_region_counters: true
  sample_errors: 0.05

Post your filled YAML + any trade-offs (e.g., mobile vs desktop, crypto vs equities). We’ll review and suggest tweaks.


One-hour build (join in)

If you’ve got an hour and want something concrete:

  1. Add the stale chip to your quote component (text + tooltip).
  2. Implement the second provider with timeouts (even a mock server).
  3. Store last-known-good in a tiny KV/local cache.
  4. Paste a 10s GIF here. We’ll give quick notes.

What you get back

  • Peer review on your policy & code snippet
  • A reusable fallback policy you can ship this week
  • A shout-out in our next Pocket Portfolio dev post (and we’ll link your repo)

Why this group exists (and where we’re going)

We care about open finance, DeFi, cryptography, and accessible design. We’ll run Spec Clinics, Paper Club, and Ship Fridays. Next threads will cover:

  • Broker CSV normalization (share one redacted row)
  • Keyboard-first dashboards (a11y = speed)
  • Wallet UX & security (MPC/AA, seedless recovery)

Introduce yourself below (stack, timezone, what you’re building). Then drop your fallback policy reply. Let’s make “never 0.00” the default. ️✨



22s (Pocket Portfolio)
App: pocketportfolio.app/app • Repo: github.com/PocketPortfolio/Financialprofilenetwork

If you read this far, tweet to the author to show them you care. Tweet a Thanks

Interesting approach to tackling the 0.00 issue with a layered fallback system and clear UX cues. How do you see this method adapting when dealing with real-time crypto data that changes multiple times per second?

Thanks Muzzamil—totally agree: crypto ticks can change many times per second.
Here’s a simple pattern that scales without going HFT and is easy to copy:


TL;DR (3 layers)

  1. Ingest (fast)

    • Subscribe to 2–3 WS feeds + keep HTTP as backup.
    • Normalize each tick: {sym, price, ts, src, seq} and store last-known-good (LKG).
  2. Coalesce (edge, 100–250 ms frames)

    • Every frameMs pick the freshest tick per symbol from the buffer.
    • If no new tick for rotateMs (e.g., 1 s) → rotate provider.
    • If no tick for staleMs (e.g., 2 s) → serve LKG with stale:true.
  3. Deliver (human-speed)

    • Send to clients via SSE/WebSocket at ~4–5 Hz (humans can’t read 20 Hz).
    • UI shows a tiny “stale” chip if stale:true.
    • If gap >10 s, gray out the cell.

This keeps ingestion real-time, but the UI calm and trustworthy.


Drop-in edge sketch (TypeScript)

// constants you can tune per market
const frameMs = 200;     // coalesce window (5 Hz)
const rotateMs = 1000;   // try next provider if no fresh tick in 1s
const staleMs  = 2000;   // mark stale after 2s without a tick

type Tick = { sym: string; price: number; ts: number; src: 'p1'|'p2'|'p3'; seq: number };

const buf = new Map<string, Tick>();     // newest per sym during frame
const lkg = new Map<string, Tick>();     // last-known-good

// ingest from multiple WS feeds (pseudo)
function onProviderTick(t: Tick) {
  if (!Number.isFinite(t.price)) return;
  const prev = buf.get(t.sym);
  if (!prev || t.ts > prev.ts) buf.set(t.sym, t); // keep latest in frame
}

// coalesce + provider rotation + stale logic
setInterval(async () => {
  const now = Date.now();
  for (const [sym, latest] of buf) {
    const lastGood = lkg.get(sym);
    const age = now - latest.ts;

    let out: Tick & { stale: boolean } =
      age <= staleMs ? { ...latest, stale: false }
                     : { ...(lastGood ?? latest), stale: true };

    // rotate if no fresh tick recently (edge-side policy)
    if (age > rotateMs) rotateProvider(sym); // implement round-robin/health check

    lkg.set(sym, { sym: out.sym, price: out.price, ts: out.ts, src: out.src, seq: out.seq });
    sendToClients(sym, out); // SSE/WS push to all subscribers
    buf.delete(sym);
  }
}, frameMs);

// HTTP fallback if WS is down
async function httpPoll(sym: string) {
  try {
    const r = await fetch(`${P1}/q?symbol=${sym}`, { signal: Abort(1400) });
    if (r.ok) onProviderTick({ ...(await r.json()), src: 'p1', seq: inc(sym) });
  } catch { /* ignore, coalesce loop handles stale + LKG */ }
}

Client side: render updates, and when stale:true, show “Last update 2.3 s • p2” (tooltip: “provider fallback active”). Never render 0.00.


Why this works (future-proof)

  • Real-time in, human-rate out: we ingest all ticks but render at a stable frame rate.
  • Deterministic failover: rotation on silence, not just errors.
  • Explicit uncertainty: stale:true beats silently wrong values.
  • Portable: works for equities/FX/crypto—just tweak frameMs/rotateMs/staleMs.

If you want, I can post a tiny repo with this coalescer + SSE server so folks can fork and plug in their providers.

More Posts

Designing a “Never-0.00” Price Pipeline in the Real World

Pocket Portfolio - Oct 1

Coming Soon: Price Pipeline Health Widget

Pocket Portfolio - Oct 3

Today’s ship: Mock-Trade Lab, 5-min CSV Import, and bulletproof Price Fallbacks

Pocket Portfolio - Sep 29

Welcome to CoderLegion 22s

Pocket Portfolio - Sep 28

React + TypeScript — The Practical Refresher

Sibasish Mohanty - Sep 9
chevron_left