Intelligent execution terminal on Solana. LIMINAL slices large or time-sensitive token swaps into TWAP chunks, parks idle capital in Kamino to earn real yield during the window, routes each slice through DFlow for MEV-protected execution with measurable price improvement, and exposes the entire flow through a Solflare-native UX with live analytics.
Every slice earns value on two axes at once — DFlow price improvement and Kamino yield — and LIMINAL surfaces both in real time.
A standard swap aggregator asks: "What's the best price right now?" LIMINAL asks: "What's the best price over the next N minutes, and how do we earn on the idle capital while we wait?"
For every execution you configure:
- Deposit — the full input amount is deposited into the optimal Kamino lending reserve (selected automatically by highest supply APY on Kamino Main Market).
- TWAP monitoring — the machine waits through each slice's target time, polling live Pyth prices every 5 seconds.
- Batched slice execution — at each slice the Kamino partial withdraw and DFlow swap are packed into a single versioned transaction (atomic, one signature), with mandatory pre-broadcast simulation.
- Final withdraw — any residual + accumulated Kamino yield is returned to your wallet.
- Analytics — every slice's price improvement (bps + USD), yield contribution, and completion time is captured for the live panel and saved to localStorage history.
A 4-slice execution takes 6 signatures total (1 deposit + 4 batched slices + 1 final withdraw) instead of 10 — the batching utility halves transaction count versus naive sequential withdraw→swap pairs.
| Partner | Role | Integration depth |
|---|---|---|
| Solflare | The single user touchpoint | Wallet adapter, session persistence, in-app browser auto-connect, pre-broadcast simulation, transaction batching UX, 2-step disconnect confirm |
| QuickNode | Data + confirmation backbone | Solana mainnet RPC, Pyth price feed polling (5s), transaction confirmation, confirmed commitment everywhere, env-driven endpoint |
| Kamino | Idle capital yield | @kamino-finance/[email protected] with @solana/kit RPC bridge, KaminoMarket.load on Main Market, reserve APY / liquidity / utilization surfaced live, getUserVanillaObligation for real on-chain position tracking |
| DFlow | MEV-protected execution engine | Quote comparison (market baseline vs DFlow endorsed quote), real price improvement bps, slippage defer discipline, no Jupiter fallback |
A four-color pastel palette drives the entire UI; every component references CSS variables, never hardcoded hex.
| Token | Hex | Role |
|---|---|---|
--color-1 |
#F6FFDC — yellow |
body background (widest surface) |
--color-2 |
#FFFFFF — white |
panel surface |
--color-3 |
#DAF9DE — mint |
elevated card |
--color-4 |
#CFECF3 — sky |
hover / secondary surface |
--color-5 |
#F9B2D7 — pink |
accent (CTA, active state, highlights) |
Body gets a subtle atmospheric gradient (pink + sky + mint blobs on the yellow base) rather than a flat fill. Text is near-black (#1a1a1a) for AAA contrast. Semantic colors (#16a34a success, #dc2626 danger, #d97706 warning) are tuned to pass WCAG AA on every pastel surface.
Space Grotesk (Google Fonts, weights 300 / 400 / 500 / 600 / 700) for all UI copy. JetBrains Mono is reserved for raw code / hash / signature display via the .liminal-code utility class. The --font-mono CSS variable is also mapped to Space Grotesk so legacy inline fontFamily: MONO references render consistently — numeric layouts stay stable thanks to font-variant-numeric: tabular-nums.
Every wallet token resolves through a Jupiter v2 search lookup (tokens.jup.ag/tokens/v2/search) keyed by mint address. The registry handles pump.fun, verified, community, and LST tokens transparently — something the older verified-only list missed. Logos load lazily with automatic IPFS/Arweave URI normalization; when a logo 404s or CORS-fails, a stable HSL gradient avatar with the token's initial letter takes over. localStorage caches results for 24 hours.
8pt grid (--space-1 through --space-12). Three-tier radius (--radius-sm: 4 / md: 8 / lg: 12). Type scale from --text-xs: 13px through --text-3xl: 36px — larger than the typical terminal to prioritize readability.
src/
├── App.tsx Root layout (desktop / tablet / mobile)
├── main.tsx React entry
├── vite-env.d.ts Typed import.meta.env declarations
├── styles/
│ └── design-system.css Single source of truth — tokens, keyframes
├── services/
│ ├── solflare.ts Wallet adapter + session + signing
│ ├── quicknode.ts RPC + Pyth + price polling (env-driven)
│ ├── kamino.ts Kamino v7 SDK via @solana/kit RPC bridge
│ ├── dflow.ts Quote + swap + TWAP slice math
│ ├── tokenRegistry.ts Jupiter v2 search + localStorage 24h cache
│ └── analyticsStore.ts localStorage history (FIFO 50)
├── state/
│ ├── executionMachine.ts TWAP state machine (IDLE → DONE)
│ └── analyticsNav.ts Cross-component tab pub/sub
├── utils/
│ ├── transactionBatcher.ts Kamino + DFlow batch into 1 versioned tx
│ └── errorHandler.ts parseError — every catch block routes here
├── hooks/
│ ├── useExecutionMachine.ts Module-singleton store + actions
│ ├── useKaminoPosition.ts 30s polling + deposit/withdraw mutations
│ ├── useDFlowExecution.ts
│ ├── usePriceMonitor.ts
│ ├── useTokenRegistry.ts Warm up registry for a list of mints
│ ├── useNetworkStatus.ts
│ └── useDeviceDetection.ts
└── components/
├── WalletPanel.tsx Left panel: balances (logo + symbol) + history
├── ExecutionPanel.tsx Middle panel: config + live timeline + CTA
├── AnalyticsPanel.tsx Right panel: live hero / history / protocol
├── HeaderBar.tsx Sticky brand + network + wallet badge
├── VaultPreview.tsx
├── QuoteComparison.tsx
├── ExecutionTimeline.tsx
├── ExecutionSummaryCard.tsx
├── ErrorCard.tsx
├── StepIndicator.tsx
├── ProgressRing.tsx
├── Sparkline.tsx
├── AnimatedNumber.tsx Respects prefers-reduced-motion
├── Tooltip.tsx
├── Button.tsx
├── CountdownTimer.tsx
└── ToastProvider.tsx Region-role notifications with semantic icons
executionMachine.ts is the brain. It moves through:
IDLE → CONFIGURED → DEPOSITING → ACTIVE
↑ ↓
└── SLICE_WITHDRAWING → SLICE_EXECUTING (loop)
↓
COMPLETING → DONE
↓
ERROR (recoverable via retry)
Pure transitions (configure, reset) compute new state; async effects (depositEffect, executeNextSlice, completeEffect, retryEffect) run RPC work and call setState. Cancellation is cooperative — every yield point re-reads status from a module-level ref. Recovery is automatic: state serializes to localStorage on each transition, and on page reload an in-flight execution surfaces a banner.
src/services/kamino.ts is wired against @kamino-finance/klend-sdk v7.3 with the @solana/kit RPC client (v7 requirement). A module-level bridge turns our QuickNode HTTPS endpoint into createSolanaRpc(...). Main Market is cached for 60s between reloads, and KaminoMarket.load() fetches every reserve with its live APY, available liquidity, and utilization ratio.
The write path (deposit, partialWithdraw, finalWithdraw, buildPartialWithdrawInstructions) is currently held at the @solana/kit Instruction → @solana/web3.js VersionedTransaction bridge — see Roadmap. The read path (vault listing, optimal selection by APY, on-chain obligation tracking) is fully functional.
- Node.js 18+
- npm 9+
- A QuickNode Solana Mainnet endpoint (free tier is enough — 10M requests/month, 25 req/s)
- A Solflare wallet (browser extension or mobile in-app browser) with some SOL for gas
git clone https://github.com/wienerlabs/liminal.git
cd liminal
npm install
cp .env.example .env.local
# Edit .env.local and paste your QuickNode HTTP Provider URL
npm run devThe Vite dev server boots at http://localhost:5173/ (or 5174 if 5173 is in use). Open it in any browser with the Solflare extension installed, or in the Solflare mobile in-app browser — the wallet auto-connects without a prompt in that context.
Copy .env.example to .env.local (gitignored automatically) and fill in:
# REQUIRED
VITE_QUICKNODE_RPC_URL=https://your-name.solana-mainnet.quiknode.pro/your-token/
# OPTIONAL (defaults shown)
# VITE_DFLOW_API_URL=https://pond.dflow.netIf VITE_QUICKNODE_RPC_URL is missing, the app throws a descriptive error on every RPC call — no silent failures. Typed via src/vite-env.d.ts for autocomplete.
npm run build # tsc --noEmit && vite build
npm run preview # serve the production build locally
npm run typecheck # tsc --noEmit onlyProduction bundles split across:
| Chunk | Raw | Gzip |
|---|---|---|
index.js (app code) |
~450 kB | ~92 kB |
vendor-recharts |
754 kB | 167 kB |
vendor-kamino (klend-sdk + scope + kliquidity) |
9.2 MB | 1.4 MB |
vendor-confetti |
14 kB | 5 kB |
index.css |
5.7 kB | 1.94 kB |
The Kamino vendor chunk is heavy because the SDK ships a full on-chain state model. vite.config.ts excludes the Orca Whirlpools WASM bindings from optimizeDeps pre-bundling to sidestep an esbuild top-level-await conflict.
- Connect Solflare from the left panel. SOL + SPL balances populate with Jupiter-resolved symbols, logo images, and live USD values from Pyth where feeds exist.
- Pick a token pair in the middle panel. The live price starts streaming from Pyth every 5 seconds. Tokens without a Pyth feed show
no feedinstead of endless skeleton. - Enter an amount, pick a window (30m / 1h / 2h / 4h), choose a slice count, set a slippage threshold (10–300 bps).
- If anything is missing, an amber hint beneath
START EXECUTIONtells you exactly what — wallet, token, amount, vault, etc. No silent-disable. - The middle panel tells you exactly how many transactions you'll sign (
1 + sliceCount + 1) — the batching discipline at work. - Click
START EXECUTION. Solflare opens. Approve the Kamino deposit. The state machine transitions toACTIVE, the TWAP loop starts, and the step indicator lights upDeposit → Monitor → Execute → Repeat → Withdrawin real time. - Each slice waits until its target time, fetches a DFlow quote, batches the Kamino withdraw and DFlow swap into one versioned transaction, simulates it, asks Solflare to sign, broadcasts, and confirms. The timeline on the right updates each slice with bps earned, USD value, and elapsed time.
- When the last slice completes, the final withdraw pulls residual + accumulated yield out of Kamino. The panel switches to
ExecutionSummaryCard, confetti fires (pink / mint / sky palette), history is saved, and the analytics Protocol tab aggregates totals.
At any point during an ACTIVE execution you can close the tab — a beforeunload warning fires — and the next time you load the app it asks whether to resume.
- Role semantics —
role="tablist"+aria-selectedon both mobile and desktop tab groups,role="dialog"+aria-modal+aria-labelledbyon the history detail modal. - Focus management — modal traps Tab, closes on Escape with
stopPropagation, restores focus to the opener on close, locks body scroll while open. - Live regions — price list is
aria-live="polite", toasts differentiatepolitevsassertiveby type, skeletons carryaria-busy. - Touch targets — 44 px minimum on the slider thumb, mobile tabs, delete and modal-close buttons.
- Reduced motion —
AnimatedNumbersnaps instead of interpolating whenprefers-reduced-motion: reduceis set. CSS animations are globally capped to 0.01 ms under the same media query. - Contrast — muted text is
#5b6470on pastel backgrounds (WCAG AA for body text). Accent pink#F9B2D7with near-black text passes AAA.
Every catch block in the state machine, services, and hooks routes through parseError(err, sliceIndex?, phase?) in src/utils/errorHandler.ts. It pattern-matches against Solana RPC errors, Solflare rejections, DFlow quote failures, Kamino liquidity issues, and batch simulation errors, producing a normalized ExecutionError with:
- A stable
ErrorCodeenum value - A user-facing English message
- A
retryableboolean - Optional
sliceIndexand timestamp
ErrorCard.tsx renders it: warning triangle on an amber background for retryable, red ✕ on a danger background for non-retryable — distinct visual hierarchy. The retry button re-enters the correct effect (retryEffect routes by code). For KAMINO_INSUFFICIENT_LIQUIDITY and KAMINO_WITHDRAW_FAILED a small reassurance note appears — "your funds in Kamino are safe; manual withdrawal may be required."
Slippage excess is deliberately not an error. The machine defers the slice 30 seconds, surfaces an inline amber banner, and continues. Only a quote fetch that hard-fails (network, endpoint down) escalates to ERROR state.
Three breakpoints driven by useDeviceDetection:
- Desktop ≥1024 — three panels side by side (
300 / 1fr / 300, symmetric). Header is sticky withbackdrop-filter: blur(8px). - Tablet 768–1023 — execution + analytics 50/50, wallet panel hidden (still accessible through the Connect CTA embedded in the middle panel).
- Mobile <768 — single column, bottom tab bar (
Wallet / Execute / Analytics) with safe-area-inset padding, and a sticky-below-headerExecution activebar during any in-flight status. Solflare's mobile in-app browser is detected vianavigator.userAgent+window.solflare.isSolflare, and wallet auto-connects without a prompt.
| Area | Stack |
|---|---|
| Runtime | React 18, Vite 5, TypeScript 5 |
| Solana | @solana/web3.js, @solana/kit, @pythnetwork/client |
| Kamino | @kamino-finance/[email protected] (@solana/kit RPC) |
| DFlow | HTTP client against https://pond.dflow.net |
| Token metadata | Jupiter v2 search API (tokens.jup.ag/tokens/v2/search) |
| Charts | recharts |
| Animation | canvas-confetti, CSS keyframes |
| Polyfills | vite-plugin-node-polyfills (Buffer/crypto/stream) + vite-plugin-wasm + vite-plugin-top-level-await |
| Fonts | Space Grotesk + JetBrains Mono via Google Fonts |
LIMINAL is architected so MEV defense lives at two independent layers and each can be upgraded without touching the other:
-
Routing (active today) — every slice clears through Jupiter Ultra's RFQ pool, which includes DFlow-endorsed market-maker quotes. Fills settle against committed inventory so sandwich and backrun opportunities don't exist at the route level. See
services/dflow.ts. -
Slot (Constellation-ready) — Anza's Constellation SIMD replaces the single-leader monopoly on block construction with multiple concurrent proposers per slot. When the proposal lands on mainnet,
transactionBatcher.ts+ the Kamino/DFlow broadcast calls will add a proposer-selection hint onsendRawTransactionso slices land on the least-censoring proposer of the epoch. The quote/build/simulate pipeline stays unchanged.
The current strategy is driven by VITE_MEV_PROTECTION_MODE:
| Value | Meaning |
|---|---|
jupiter-ultra (default) |
Today's production path — routing-only defense |
jupiter-ultra+constellation |
Hybrid — both layers active once Constellation is live |
constellation-only |
Slot-level only (used for measurement / ablation) |
The Analytics Protocol tab renders a live "MEV Protection" card describing which layers are active and which are ready. A small MEV: … chip in the navbar surfaces the same state at a glance.
- Onchain activity — hit the BLOK 8 target (5+ wallets × 50 txs on mainnet) before the hackathon submission deadline.
- WalletConnect deep-link — mobile onboarding via
solflare://browse?url=liminal.app(spec'd in BLOK 6, not yet implemented). - Token allowlist / rug filter — surface a warning when the destination mint is unverified (Jupiter
isVerifiedflag, Rugcheck risk score). - On-chain history rebuild — today analytics history lives in
localStorage(FIFO 50). Reconstruct past executions from wallet signatures + our program IDs so users don't lose history when switching devices.
.env.localis gitignored via the*.localpattern and again explicitly via.env*— secrets never reach git..env.exampleis committed as the template.- The development-only global error trap in
index.htmlis guarded tolocalhost/127.0.0.1/.localhostnames and ring-buffered to 200 entries; it no-ops on any deployed origin. HeaderBar,WalletPanel, and the execution flow show the connected wallet's public address only — full address is copy-on-click and truncated in display.
See LICENSE.