What it is
Lucky Cat is the tier 3 capstone example: $MEOW, a simulated Maneki-neko trading floor built entirely on Chirp and ChirpUI. Full pages, HTML fragments, a streaming portfolio dashboard, and a live cross-page ticker — with no client-side framework and no build step. Prices, fills, and balances are simulated in memory, so you can clone it, run it offline, and read the code.
Reach for it when you want to see how a real, multi-page product wires return types, live server state, and the secure-by-default stack together.
Live demo: luckycat-production.up.railway.app
Location:examples/chirpui/lucky_cat/
It ships:
- a full-viewport app shell — brand topbar, cross-page ticker strip, and a collapsible navigation rail
- a Markets Home lobby and a market-detail page (
/markets/{symbol}) with a server-rendered SVG price chart, a depth-bar order book, and a recent-trades tape - a place/cancel-order trade flow built on Chirp return types
- a Suspense portfolio dashboard whose panels paint as skeletons and stream in as their data resolves
- a cross-page ticker, balance, and notification bell bound to server-owned signals over one SSE connection
- public-browse / gated-trading authentication across three gating levels
What this replaces
If you would reach for React/Next (or a separate SPA + API) for a trading-floor product UI, Lucky Cat shows the Chirp alternative.
| Concern | Typical React / Next stack | Lucky Cat on Chirp |
|---|---|---|
| Tooling | npm install, bundler, build step, node_modules |
No build step, nonode_modules— Python + static CSS/JS only |
| Live chrome | Client state + WebSocket or client-managed SSE handlers | Server-ownedsignal() values on one /_chirp/liveSSE connection |
| Navigation | Client router, hydration, layout re-fetch | Boosted htmx swaps#main; server re-renders the rail from the current path |
| Forms & validation | Client form lib + separate API routes | Return type as intent in one handler:ValidationError (422) or FormAction |
| Page-local live data | WebSocket subscriptions + client reconciliation | EventStreampushes HTML fragments — no client state graph |
| Slow dashboard | Loading spinners or client suspense boundaries | Suspense: shell paints with skeletons, panels stream as OOB swaps resolve |
| Auth | JWT in storage, client route guards | Session cookie,@login_required, AuthMiddlewarein the secure stack |
| Charts | Chart.js / Recharts in the browser | Server-rendered SVG — no JS chart library |
| Deploy surface | Node server (+ often a separate API tier) | Single Python process; demo pinsworkers=1for in-memory state |
| Tests | Jest/RTL + mocked fetch/WebSocket | pytest against a deterministic SimFeed— offline, CI-safe |
| Real exchange / web3 | Wallet-connect, chain RPC, matching engine | Out of scope — simulated prices, in-memory wallet, no chain |
Try it
Run the example, test it, or scaffold the same foundation for a new app.
pip install "bengal-chirp[ui]"
PYTHONPATH=src python examples/chirpui/lucky_cat/app.py
# open http://127.0.0.1:8000/ — sign in with neko / luckycat to tradepytest examples/chirpui/lucky_cat/pip install "bengal-chirp[ui]"
chirp new myapp --shell
cd myapp && python app.pyBrowsing the markets needs no account. Sign in (demo creds:neko/
luckycat) to trade, deposit, and view your portfolio. chirp new --shell
wiresuse_chirp_ui(app), boosted navigation, and the secure-by-default stack —
the same foundation Lucky Cat builds on.
Authentication
Lucky Cat is public-browse, gated-trading: anyone can browse the markets grid and a coin's detail page, but the account section and every mutation require sign-in. It shows the full range of gating, not a blanket lock:
- Full-page gating —
@login_requiredon the account handlers (/trade,/portfolio,/activity,/markets/favorites,/settings). An anonymous hit is a 302 to/login?next=<path>. - Component gating —
current_user()conditionals: the topbar swaps "Sign in" for the user menu, and the watchlist star on the public grid becomes a "sign in to star" link. - Action gating —
@login_requiredon the mutation routes as the backstop.
How it's wired
Internals: return types, Suspense, signals, and the auth stack
The internals below are for readers who want the exact mechanics. The trade-panel tutorial walks the same code paths from scratch.
Trade flow. A clean fill returns oneFormActionwhose multi-target OOB set
swaps positions, balance, the open-order badge, and a toast. An invalid order
returnsValidationErrorfor a 422 re-render with field errors and submitted
values preserved.
Suspense dashboard. Six panels render as skeletons in the shell, then stream
in as OOB swaps when their awaitables resolve. Use{% if x is deferred %}for
the loading-vs-loaded test; passdefer_blocks / defer_mapwhen static
analysis can't discover a panel through macro arguments. See
Suspense Dashboard.
Live chrome. The cross-page ticker, $MEOW balance, and notification bell bind
to server-ownedsignal()s over one /_chirp/liveSSE connection
(declare-once / bind-many), with a purederivedsignal for the bell's
unread-count pill. See Signals.
Login flow. Bad credentials returnValidationError(a 422 in-place
re-render). A clean sign-in callslogin() and returns FormActionwith no
fragments — htmx gets anHX-Redirect(a full reload so the persistent topbar
repaints), a plain POST gets a 303.
Stack ordering.AuthMiddlewarejoins the secure-by-default stack as
Session → Auth → CSRF → SecurityHeaders. Passwords are hashed via
chirp.security.passwords — argon2id when chirp[auth]is installed, stdlib
scrypt as the always-available fallback. A single in-memory demo account keeps
the example single-process (matchingworkers=1).
Deploy it
The example ships aDockerfile and railway.tomlso it runs as a standalone
Railway service:python app.py binds 0.0.0.0:$PORTthrough
AppConfig.from_env(), and the healthcheck targets /health.
See Production Deployment for the full production shape.
Source
Build along
Build a Live Trade Panel in 20 Minutes — a
from-scratch walkthrough of the markets grid (Page at GET /) and the
POST /trade/order return-type pair (ValidationError422 in place →
FormActionmulti-target OOB). Grounded in this example's real code paths.