Trade-offs Interview Reference#
All key trade-offs across Loky AI, DappLooker, and HyprEarn — with full reasoning, failure modes, and how to say it in an interview.
How to Use This Document#
- Each trade-off has: What you chose, What you gave up, Why, When it breaks, and How to say it
- The "How to say it" section is written in first person — practice saying it out loud
- Grouped by category so you can pick the ones most relevant to the question being asked
What Counts as a Trade-off#
A trade-off is when two options are both valid and you're giving something real up regardless of which one you pick. The test:
"Could a reasonable engineer have chosen the other option and been right?"
If yes — it's a trade-off. If no — it's a correct design decision, not a trade-off. Don't present correct design decisions as trade-offs in interviews. Present them as "here's the problem, here's why this was the right approach" — which is equally impressive.
Category 1 — Architecture Trade-offs#
1.1 Monolith Over Microservices#
Projects: Loky AI, DappLooker
| Chose | Single deployable monolith |
| Gave Up | Independent scaling per service, fault isolation |
| Why | Development speed at early stage. Microservices add deployment, networking, and operational complexity that isn't justified until you know which parts of the system need to scale independently. |
| When It Breaks | When the LLM service needs 10x resources but you can't scale it independently. A spike in LLM usage forces you to scale the entire monolith — wasteful and expensive. Also breaks when one service crashes and takes everything down with it. |
How to say it:
"We went with a monolith on both Loky and DappLooker. At that stage, the overhead of microservices — separate deployments, network calls between services, distributed tracing — wasn't justified. The real cost only shows up when different parts of your system have wildly different resource needs. The LLM service is the obvious example — if that needs 10x compute, you're stuck scaling the whole monolith instead of just that one service. We accepted that trade-off consciously. The upgrade path was clear: extract the LLM service first when scale demands it."
1.2 TheGraph Over Own Blockchain Indexer#
Projects: Loky AI, DappLooker
| Chose | TheGraph managed indexing (subgraphs) |
| Gave Up | Control over sync speed, custom indexing logic |
| Why | Running your own blockchain indexer is serious infrastructure. It requires maintaining a full node or archive node, writing and debugging custom parsing logic, handling reorgs, and operating it reliably. TheGraph handles all of this as managed infrastructure. At our scale, the engineering cost of owning this wasn't justified. |
| When It Breaks | TheGraph downtime kills your analytics pipeline. During periods of high network activity, subgraphs can fall behind. You have no lever to pull — you're dependent on TheGraph's SLA. Also breaks when you need indexing logic that subgraphs don't support. |
How to say it:
"We used TheGraph for blockchain indexing rather than running our own indexer. That's a significant infrastructure decision — running your own indexer means maintaining nodes, handling chain reorgs, writing custom parsing logic. TheGraph takes all of that away. The cost is that we're dependent on their availability and their subgraph constraints. If TheGraph goes down, our analytics pipeline goes down. We consciously accepted that dependency because the build cost of owning indexing infrastructure wasn't worth it at our stage."
1.3 Database as Coordinator Over Redis / Job Queue#
Project: HyprEarn
| Chose | Token-level distributed locking in the database |
| Gave Up | Clean job queue architecture (Redis, Bull, etc.) |
| Why | Adding a job queue means adding another piece of infrastructure to operate and monitor. For an MVP with clear, simple coordination needs — "don't let two machines process the same token" — the database was already there and could handle this with time-bound locks. No new dependency, no single point of failure. |
| When It Breaks | When you have complex job dependencies, priority queues, or retry logic. A database lock can tell you "this token is being processed" but can't tell you "retry this failed job with exponential backoff after 3 failures." That's where a proper job queue earns its complexity. |
How to say it:
"In HyprEarn, we needed to coordinate 100 tokens across multiple cron machines without two machines processing the same token simultaneously. The 'right' solution would've been a Redis queue or a job scheduler. But we were running an MVP — we didn't want to introduce another piece of infrastructure. Instead, each machine would query the database for unlocked tokens, acquire a time-bound lock on 20 of them, process them, and release the locks. Time-bound was important — if a machine crashed mid-processing, the lock released automatically after 1 minute. No manual intervention. Scales horizontally by just adding machines. Clean enough for the problem we had."
1.4 Self-Hosted ClickHouse Over Managed Service#
Project: DappLooker
| Chose | Self-host ClickHouse on own infrastructure |
| Gave Up | Operational simplicity, managed monitoring, automatic upgrades |
| Why | ClickHouse Cloud and other managed options add cost at scale. Self-hosting gave us full control over configuration, no vendor pricing surprises, and no data egress costs. At our query volume the cost difference was meaningful. |
| When It Breaks | When something goes wrong at the infrastructure level — you own the debugging. We experienced this directly with the mutation degradation issue. A managed service might have surfaced that problem through built-in monitoring before it became 4-5 months of silent degradation. |
How to say it:
"We self-hosted ClickHouse rather than using ClickHouse Cloud or a managed service. The cost control was the main driver — at our query volume, managed pricing would've added up. The trade-off is that we owned all the operational complexity. When we hit the mutation degradation issue, there was no support team to call — we had to diagnose it ourselves. In hindsight, a managed service with better built-in monitoring might have surfaced that problem earlier. That's the real cost of self-hosting: your monitoring is only as good as what you build."
1.5 Single Batched LLM Job for All 10 Tokens Over Parallel Per-Token Jobs#
Project: HyprEarn
| Chose | Send all 10 tokens to LLM in one job, get comparative suggestions |
| Gave Up | Speed — parallel per-token jobs would complete faster |
| Why | The LLM's value wasn't just analyzing each token in isolation — it was producing ranked, comparative suggestions across tokens. A single prompt with all 10 lets the model reason across them: "Token A has better volume/OI ratio but Token B has deeper order book liquidity." Parallel per-token jobs lose that cross-token reasoning entirely. |
| When It Breaks | When the batch job takes longer than expected and blocks the 7-minute refresh cycle. Also, if one token's data causes the prompt to grow too large, the whole job is affected rather than just that token's job. |
How to say it:
"We sent all 10 filtered tokens to the LLM in a single batch job rather than 10 parallel per-token jobs. The faster option would've been parallel — each token independently, results merged. But we were generating comparative, ranked suggestions — the LLM needed to see all 10 together to say 'Token A is better than Token B because...' Parallel jobs would've given us 10 independent analyses with no cross-token reasoning. The trade-off is the batch job is slower and if it grows too large, every token is affected. For the quality of suggestions we needed, batch was the right call."
Category 2 — Data & Storage Trade-offs#
2.1 ClickHouse Over Postgres for Analytics#
Project: DappLooker
| Chose | ClickHouse for analytics queries |
| Gave Up | Simpler stack, familiar Postgres update model |
| Why | Analytics queries on schemas larger than 50GB were consistently breaching 60 seconds in Postgres. Analysts were waiting. We evaluated Druid, Pinot, BigQuery, Snowflake, Redshift, and ClickHouse. Druid and Pinot use pre-aggregation — they're built for known high-QPS patterns, not ad-hoc analyst queries on unknown dimensions. BigQuery and Snowflake had a 3–10 second latency floor even when optimized, plus unpredictable per-TB pricing at our query volume. ClickHouse gave us sub-second on raw unaggregated data, ~7x compression, and was self-hostable. |
| When It Breaks | When you have update-heavy workflows. ClickHouse's update model — mutations — rewrites data in the background. We hit this: workflows that did updates worked fine in Postgres but silently queued up mutations in ClickHouse, causing 4–5 months of gradual latency degradation before we identified the cause. |
How to say it:
"When our analytics schemas crossed 50GB, Postgres queries were breaching 60 seconds. We evaluated four options: Druid and Pinot lost because they're built on pre-aggregation — you can't do ad-hoc queries on dimensions you didn't pre-define. BigQuery and Snowflake had a 3–10 second latency floor and unpredictable pricing at our query volume. ClickHouse won — sub-second on raw unaggregated data, about 7x compression, self-hostable. Same queries went from 60 seconds to 2–3 seconds. But we hit a production issue post-migration. ClickHouse's update model — mutations — rewrites data in the background instead of in-place. Workflows that did updates silently queued mutations and caused gradual latency degradation over 4–5 months. We fixed it by switching to ReplacingMergeTree — append new versions, deduplicate on merge — so we never needed mutations again."
2.2 ReplacingMergeTree Over Mutations#
Project: DappLooker
| Chose | Append new versions + deduplicate on merge |
| Gave Up | In-place update semantics (familiar from Postgres) |
| Why | ClickHouse mutations rewrite data in the background. They queue silently, pile up under write-heavy workflows, and compete for the same merge threads as normal operations. ReplacingMergeTree treats updates as append + deduplicate — no rewrites, no background queue buildup. |
| When It Breaks | When you need strong read consistency on the latest version. ReplacingMergeTree deduplicates on merge — until a merge happens, you might read multiple versions of the same row. You can use FINAL keyword to force deduplication on read, but that has a query cost. |
How to say it:
"After identifying mutations as the cause of our ClickHouse degradation, we audited every workflow touching migrated tables and switched update patterns to use ReplacingMergeTree. Instead of mutating a row, you append a new version with a higher version number and let ClickHouse deduplicate on merge. No background rewrites, no silent queue buildup. If I were doing the migration again, I'd audit every workflow for updates before go-live — not after 5 months of degradation."
2.3 Market Cap Tiering for Candle Data#
Project: Loky AI
| Chose | Top 700 tokens by market cap for 5m/15m resolution; all 2000+ tokens for 1hr |
| Gave Up | High-resolution candle data for newly viral tokens with small market caps |
| Why | Covering 2000+ tokens at 5m resolution would require enormous API call volume. We used market cap as a proxy for "tokens users actually care about" — a reasonable approximation. The 1hr resolution covers everything because lower frequency = fewer calls = feasible at full breadth. |
| When It Breaks | A token goes viral on social media while its market cap is still small. Users want 5m candles to analyze it. We only have 1hr resolution until the mcap grows into the top 700. The correct fix would've been adding a volume spike signal as a second tiering criteria — but we accepted this limitation to avoid over-engineering. |
How to say it:
"We had 2000+ tokens needing candle data at multiple resolutions. Covering all of them at 5-minute intervals wasn't feasible given API rate limits. So we tiered it — top 700 by market cap get 5m and 15m resolution, all 2000+ get 1-hour resolution. Market cap is a reasonable proxy for user interest. The known edge case is a viral new token with a small mcap — it misses high-resolution candles until it grows into the top 700. We consciously accepted that. A volume spike signal would fix it, but that was over-engineering for where we were."
2.4 Cleanup Worker Deleting Candle Data Older Than 1 Week#
Project: Loky AI
| Chose | Delete resistance/support level data older than 7 days |
| Gave Up | Historical candle data for backtesting |
| Why | Resistance and support levels are only meaningful in the present — a support level from 3 months ago is irrelevant to current trading decisions. Keeping all historical data would grow storage linearly with no user benefit for this specific use case. |
| When It Breaks | If users want to do historical backtesting or see long-term price patterns. We weren't building that product — we were building a real-time analysis tool. The 7-day window was deliberately scoped to current relevance. |
How to say it:
"We had a background worker deleting candle data older than 1 week. That's a deliberate product decision — resistance and support levels are only meaningful in real-time. A support level from months ago doesn't help a trader making decisions today. Keeping all historical data would've grown storage indefinitely for no user benefit. No race condition risk either — the gap between data being produced and deleted is 7 days. We accepted that this meant we couldn't do historical backtesting with this data, which wasn't our use case."
Category 3 — API & External Integration Trade-offs#
3.1 Prioritized Fallback Chain Over Single Data Source#
Project: Loky AI
| Chose | CoinGecko → GeckoTerminal → DexScreener → CoinMarketCap fallback chain |
| Gave Up | Simpler single-source integration |
| Why | No single aggregator has complete coverage, especially at token launch. New tokens exist on-chain before aggregators index them — CoinGecko might list a token 10 minutes after launch, GeckoTerminal 5 minutes, DexScreener 2 minutes. This isn't API failure — it's temporal unavailability. Different problem, different solution. |
| When It Breaks | If all sources fail simultaneously — unlikely but possible. Also breaks if a source's indexing speed changes and we haven't updated the fallback order. The order was based on indexing speed + reliability + cost at the time we built it. |
How to say it:
"We integrated 4 data sources for token data — CoinGecko, GeckoTerminal, DexScreener, CoinMarketCap — in a prioritized fallback chain. The key insight was that this wasn't about handling API failures. It was about temporal unavailability. A new token exists on-chain the moment it's deployed, but aggregators have different indexing lags — CoinGecko might take 10 minutes, DexScreener 2 minutes. The data exists, it just hasn't been indexed yet. A miss at T+1 minute might succeed at T+5 minutes. We had to cache failed lookups carefully to avoid hammering sources with retries on temporally unavailable data."
3.2 Stablecoins Only for Crypto Payments#
Project: DappLooker
| Chose | Accept only USDC and USDT |
| Gave Up | Broader payment options (ETH, BTC, other tokens) |
| Why | Accepting volatile tokens means you need real-time exchange rates, conversion math, handling underpayment when price drops between initiation and confirmation, overpayment refund logic, and accounting complexity. Stablecoins eliminate this entire class of problems. 1 USDC = $1, always. The math is trivial. |
| When It Breaks | Users in regions without easy USDC/USDT access can't pay. Also limits users who only hold ETH or BTC and don't want to convert. This was an acceptable limitation given our user base. |
How to say it:
"For crypto payments, we only accepted USDC and USDT. That was a deliberate scoping decision. Accepting ETH or other volatile tokens means you need live exchange rates, conversion math, handling price movement between when the user initiates payment and when the transaction confirms, underpayment edge cases. Stablecoins eliminate all of that — 1 USDC is 1 dollar, the math is trivial. We gave up broader payment options in exchange for eliminating an entire class of problems. For our user base, that was the right trade."
3.3 Multiple Servers for Rate Limits Instead of Paid API Tier#
Projects: Loky AI, HyprEarn
| Chose | Distribute cron jobs across multiple servers |
| Gave Up | Clean single-server architecture, guaranteed ToS compliance |
| Why | HyperLiquid had no paid API tier. For Loky, upgrading to a higher paid tier wasn't cost-justified at that stage. Distributing across servers — each appearing as a separate client — solved the rate limit problem without spending on API upgrades. |
| When It Breaks | The API provider detects the pattern and blocks all servers simultaneously. Also a ToS risk — some providers explicitly prohibit this. We knew this and accepted it as an MVP trade-off. |
How to say it:
"In HyprEarn, HyperLiquid had no paid API tier — rate limits were the only option. We solved it by distributing cron jobs across multiple machines, each processing 20 tokens, each appearing as a different client to the API. In Loky, we did something similar for candle data. The honest risk is that a provider could detect the pattern and block everything simultaneously. It's also potentially a ToS concern. We knew this and accepted it for the MVP stage. In production, the right answer is a paid tier or a different data vendor."
3.4 Reactive Monitoring Over Proactive Schema Validation (Virtuals API)#
Project: Loky AI
| Chose | Admin alerts when response shape changes |
| Gave Up | Instant detection of API changes, zero user impact window |
| Why | The Virtuals API was undocumented — reverse engineered from browser network calls. There was no contract. We built defensive parsing and set up alerts when the response shape deviated from expected. Proactive schema validation (checking the schema before relying on it) would've been stronger but wasn't prioritized given the low traffic on this feature. |
| When It Breaks | The API changes silently. There's a window between the change and the alert firing where users see broken or missing data. That window could be minutes or hours depending on traffic patterns and alert sensitivity. |
How to say it:
"Virtuals API wasn't public — we found it by inspecting browser network calls. No docs means no contract. The API could change its response shape at any time without warning. We built defensive parsing around it and configured admin alerts to fire when the response shape deviated from what we expected. Honest limitation: alerts are reactive. There's a window between a silent API change and the alert firing. Proactive schema validation on every request would've been stronger — check the shape before relying on it, fail loudly if it's wrong. We didn't prioritize that given the traffic level on this feature, but it's the right long-term answer."
Category 4 — LLM & AI Trade-offs#
4.1 4-Layer LLM Pipeline Over Single Prompt#
Project: Loky AI
| Chose | Intent → Token → Query → Response pipeline |
| Gave Up | Simpler single-call architecture |
| Why | A single prompt with all token data caused two distinct hallucination problems: data mixing (model attributed values to wrong fields when too many data points were passed together) and context window pollution (model started ignoring earlier parts of a long prompt). Breaking it into 4 layers meant by the time we hit Layer 4, context was small, precise, and contained only the data points needed to answer the specific question. |
| When It Breaks | Latency. Four LLM calls is slower than one. For a chat interface, this is visible to the user. We mitigated with streaming so users see partial responses early. Also more complex to debug — you need logging at every layer to trace where a bad response originated. |
How to say it:
"Our initial LLM implementation was a single prompt with all the token data. We saw two failure modes: the model would mix up which value belonged to which field when we passed too many data points together, and with large context windows it would start ignoring or confusing earlier parts of the prompt. We solved it with a 4-layer pipeline. Layer 1 identifies intent — what is the user asking? Layer 2 identifies which token. Layer 3 builds a precise data query — what specific data points are needed to answer this question? Layer 4 generates the response using only those data points. By Layer 4, context is small and precise. Hallucination dropped dramatically. The trade-off is latency — four calls instead of one. We used streaming to make that acceptable for the user."
4.2 CSV Over JSON for LLM Context#
Project: Loky AI
| Chose | CSV format for passing tabular data to the LLM |
| Gave Up | Hierarchical data representation, familiar JSON format |
| Why | JSON repeats every key for every row — at scale this is massive token waste. CSV states keys once as a header row, then only values. Beyond token efficiency, LLMs have seen enormous amounts of tabular CSV data in training. The flat structure pattern-matches instantly. Hierarchical JSON requires the model to mentally "unpack" nested structures before processing the values. |
| When It Breaks | When data is genuinely hierarchical. If you have nested objects that can't be flattened without losing meaning, CSV forces artificial flattening or loses the relationship. In those cases JSON is the right format despite the token cost. |
How to say it:
"We switched from JSON to CSV for passing data to the LLM. Two reasons. First, token efficiency — JSON repeats every key for every row. For 100 rows of token data, that's 100 repetitions of every field name. CSV states the header once. Second, and more important — LLMs have seen CSV-formatted tabular data far more often in training than deeply nested JSON. The flat structure pattern-matches instantly. JSON requires the model to unpack hierarchical nesting before it can reason about the values. We measured the difference in hallucination rates and CSV was meaningfully better, not just on tokens but on response accuracy."
4.3 Confidence Threshold With Fallback for AI Studio#
Project: DappLooker
| Chose | Return error + manual browse link below confidence threshold |
| Gave Up | Always returning a result (even a low-confidence one) |
| Why | AI Studio used vector embeddings (title + description) for chart search. Quality was entirely dependent on how well users described their charts. A poorly described chart matched poorly. Below a cosine similarity threshold, returning the closest match would be confidently wrong — a bad experience. Returning nothing with a path to manual browsing is more honest and more useful. |
| When It Breaks | When the threshold is too high and too many valid queries get rejected. Calibrating the threshold requires enough user data to understand the distribution of similarity scores for good vs bad matches. We were in beta with low user count, so the calibration was approximate. |
How to say it:
"AI Studio matched user natural language queries against vector embeddings of chart titles and descriptions. The quality problem was that search quality was entirely dependent on how well charts were described — a chart titled 'DAU' with no description matched poorly against almost anything. Below a cosine similarity threshold, we returned an error message with a link to browse charts manually instead of returning a low-confidence match. The principle was: never confidently wrong. A guess that's 40% likely to be right does more damage than a clean 'I couldn't find it.' The threshold calibration was approximate — we didn't have enough users to tune it precisely, which is part of why it stayed in beta."
4.4 AI Studio in Beta, Not Full Release#
Project: DappLooker
| Chose | Ship in beta, manage expectations explicitly |
| Gave Up | Feature completeness, higher quality bar |
| Why | Search quality was fundamentally dependent on user input quality — chart titles and descriptions — which we couldn't control. Low user count meant we didn't have the data to tune the threshold or the volume to justify a full investment in improving the embedding quality. |
| When It Breaks | Scales poorly if a large user base consistently creates poorly described charts. The quality problem becomes a support problem at scale. |
How to say it:
"We shipped AI Studio in beta deliberately. The core quality problem was that our vector search was only as good as the chart descriptions users wrote. We couldn't control that input quality. With a small user base, the failure rate wasn't a crisis — it was manageable. Keeping it in beta let us ship it, get signal on usage, and manage expectations without over-committing to a feature whose quality we couldn't fully own."
Category 5 — UX & Product Trade-offs#
5.1 Polling Over SSE for Suggestion Updates#
Projects: HyprEarn, DappLooker
| Chose | User sees a "please refresh" prompt when new suggestions are ready |
| Gave Up | Real-time push notification to the browser |
| Why | SSE (Server-Sent Events) adds infrastructure complexity — long-lived connections, connection management, reconnect logic, load balancer configuration for sticky connections. For HyprEarn, suggestions refresh every 7 minutes — slow enough that polling or a refresh prompt is an acceptable UX. The cadence doesn't justify the engineering overhead of real-time push. |
| When It Breaks | When events are frequent enough that polling creates meaningfully stale UX. For trading alerts that fire every 30 seconds, polling every 60 seconds means you could be 60 seconds late on a critical signal. That's where SSE earns its complexity. At 7-minute suggestion cadence, that problem doesn't exist. |
How to say it:
"We chose polling over SSE for suggestion updates in HyprEarn. Suggestions refresh every 7 minutes — when new ones are ready, users see a 'please refresh' prompt. SSE would give real-time push notification but adds real complexity: long-lived connections, reconnect logic, load balancer configuration. At a 7-minute cadence, none of that complexity is justified. The refresh prompt is a tiny UX friction that saves meaningful engineering overhead. If we were building real-time price alerts firing every 30 seconds, SSE would be the right call."
5.2 No Retry on Trade Execution#
Project: HyprEarn
| Chose | Failed trade surfaces to user immediately |
| Gave Up | Automatic recovery from transient failures |
| Why | Automatic retry on financial transactions risks double execution. If the trade executed but the confirmation was lost in transit, a retry would execute the same trade twice — a very bad outcome. For an MVP, failing clean and letting the user decide is safer than any retry logic. Proper retry would require idempotency keys and deduplication at the exchange level, which wasn't built. |
| When It Breaks | Transient network failures that would've succeeded on retry will instead surface to users. Users have to manually retry. In a fast-moving market, that manual retry window might mean a worse execution price. |
How to say it:
"We deliberately chose no retry on trade execution in HyprEarn. Most engineers would call that a gap — I'd argue it's the right call for financial transactions without idempotency infrastructure. If you retry a trade and the original actually executed but the confirmation was lost, you've now doubled the position. For an MVP where we hadn't built idempotency keys and deduplication at the exchange level, failing clean and surfacing the failure to the user was the safer path. The user sees the failure, decides whether to retry manually. That's a worse UX than automatic recovery — but it's an infinitely better outcome than an accidental double trade."
5.3 Progressive Disclosure Over Complete Data on First Load#
Project: Loky AI
| Chose | Show token details instantly, fill in analytics as they're ready |
| Gave Up | Complete data on first load, single definitive state |
| Why | Full on-chain analytics for a new token take 15–20 minutes due to TheGraph subgraph indexing latency. Blocking the UI until all analytics are ready would mean users wait 15 minutes to see anything. Progressive disclosure shows what's available immediately and fills in the rest as it arrives — perceived performance is dramatically better. |
| When It Breaks | A user makes a decision (like a trade) based on the partial data visible before analytics complete. If the bundle detection or wallet concentration data comes in later and contradicts the early data, the user may have already acted. This is the real risk of progressive disclosure on a financial analytics product. |
How to say it:
"New token analytics took 15–20 minutes to fully compute — we were processing 100k on-chain transactions and the bottleneck was TheGraph subgraph indexing latency, which we couldn't control. We chose progressive disclosure: show the token immediately with what we have, fill in wallet analysis, bundle detection, and concentration metrics as they complete. The alternative — blocking the UI for 15 minutes — would've killed the product. The honest risk is a user seeing partial data and making a decision before the full picture arrives. We used loading states and clear indicators to signal what wasn't ready yet."
5.4 Refresh Prompt Over Silently Executing on Stale Suggestions#
Project: HyprEarn
| Chose | Block trading and show refresh prompt when suggestions are near expiry |
| Gave Up | Seamless UX without interruption |
| Why | Suggestions expire at 10 minutes. Between 7–10 minutes (while new suggestions are generating), the current ones are still technically valid but about to be replaced. Silently letting users trade on suggestions that are about to change is dishonest. A refresh prompt is a small UX friction that protects users from acting on data that's in the process of being superseded. |
| When It Breaks | If the new suggestion generation job takes longer than the 3-minute overlap buffer, users hit a state where old suggestions are expired and new ones aren't ready. They can't trade at all. We handled this edge case by triggering a fresh job on user request, at the cost of making the user wait. |
How to say it:
"In HyprEarn, we chose to show users a 'please refresh' prompt when suggestions were in the 7–10 minute window — still technically valid, but about to be replaced by a new LLM run. We could've silently let users trade. We chose not to. Executing a trade on a suggestion that's 30 seconds from being superseded is misleading. A refresh prompt is honest. Small friction, but better than a bad trade. The principle was: be honest with users rather than hide the latency."
Category 6 — Build vs Buy Trade-offs#
6.1 Metabase Over Custom Visualization#
Project: DappLooker
| Chose | Metabase as the visualization layer |
| Gave Up | Full UI control, custom chart types, bespoke UX |
| Why | Building a visualization layer from scratch — charting library, dashboard editor, query builder, embed system — is a massive engineering investment. Metabase gave us all of that immediately. For a product where visualization is important but not the core differentiator, buying was the right call. |
| When It Breaks | When users need chart types Metabase doesn't support. When you need deeply custom interactions or embed behavior. When Metabase's query model doesn't map cleanly to your data structure. At that point, you've either built workarounds that create technical debt or you're blocked on features that require rebuilding. |
How to say it:
"We used Metabase as our visualization layer rather than building it ourselves. Building a charting system from scratch — dashboard editor, query builder, embed system, all of it — is easily 6+ months of engineering just to reach feature parity with an off-the-shelf solution. Metabase let us ship visualization in weeks. The trade-off is control — when users need chart types Metabase doesn't support, we're stuck. We knew that going in. The bet was that the speed advantage of buying outweighed the control disadvantage of not building, and at that stage it was the right bet."
6.2 Builder Code Delegation Over Custodial Model#
Project: HyprEarn
| Chose | HyperLiquid builder code for non-custodial trade execution |
| Gave Up | Simpler auth model (users deposit funds into our platform) |
| Why | In crypto, custody is trust. Asking users to deposit funds into a platform means they're trusting you with their money — a much higher bar for a new MVP. Builder code delegation lets users grant HyprEarn execution rights without ever giving up custody. User signs once during onboarding, can revoke at any time. Trades execute in their wallet. We never touch their private keys. |
| When It Breaks | If HyperLiquid changes or deprecates the builder code model, we lose the ability to execute trades on behalf of users. Also, if a user's builder code authorization is used maliciously (by a security breach on our end), we've executed trades we shouldn't have — worse than a custody breach in some ways because the trades look legitimate. |
How to say it:
"For trade execution in HyprEarn, we used HyperLiquid's builder code delegation model. Users sign a builder code during onboarding — one signature that registers HyprEarn as an authorized executor for their account. After that, our backend can call the HyperLiquid API and trades execute in the user's wallet. We never touch private keys, users never deposit funds. That's non-custodial trading automation. In crypto, whether you hold user funds is a fundamental trust boundary. The custodial model — users deposit into our platform — would've been simpler to build but much harder to get user trust for an unproven MVP."
Category 7 — Financial System Trade-offs#
7.1 Poll-and-Reconcile Over Real-Time Confirmation for Crypto Payments#
Project: DappLooker
| Chose | 5-minute polling window + 15-minute reconciliation job |
| Gave Up | Instant activation, webhook-style notification |
| Why | Blockchains don't send you webhooks. There's no server-side push when a transaction confirms. You have to query the chain yourself. A 5-minute polling window covers the normal confirmation time for the chains we supported. The reconciliation job handles anything that times out — covering slow confirmations or temporary RPC issues. |
| When It Breaks | Worst case activation delay is ~20 minutes (5-minute timeout + up to 15 minutes until next reconciliation run). For a payment product, that's a visible delay. In practice, chains confirmed faster than the timeout so most activations were near-instant. Also breaks if the reconciliation job itself has a bug — PENDING transactions would sit forever. |
How to say it:
"Blockchain doesn't give you webhooks. When a user pays in USDC, there's no server push telling you the transaction confirmed — you have to poll the chain yourself. We built a 5-minute polling window: wait for transaction confirmation within that window, activate the plan if confirmed. If we time out, mark it PENDING and let a reconciliation job running every 15 minutes pick it up and check on-chain status. Worst case delay was about 20 minutes. In practice, chains confirmed fast enough that most activations were near-instant. The important part was making PENDING the explicit state rather than just losing unconfirmed transactions."
7.2 Two-Gate Duplicate Payment Prevention#
Project: DappLooker
| Chose | UX gate (disable button while PENDING) + idempotency key at API level |
| Gave Up | Handling duplicates reactively in reconciliation |
| Why | The design philosophy was: don't handle duplicates, prevent them. Reactive deduplication in reconciliation means you've already processed a payment twice and are now trying to untangle it. Two upstream gates — UI gate prevents normal double-clicks and user impatience, idempotency key catches anything that gets through despite the UI gate — meant the reconciliation job never had to deal with duplicate payments. |
| When It Breaks | If a user finds a way to initiate two payments from different sessions/devices simultaneously. The UI gate only prevents same-session duplicates. The idempotency key handles this — same order ID, same request = idempotent response. |
How to say it:
"We had two gates preventing duplicate crypto payments. Gate 1: if an order is PENDING, the payment button is disabled. User sees 'please wait.' Gate 2: idempotency key at the API level — if the same order ID comes through twice, we return the same response without re-processing. The philosophy was don't handle duplicates, prevent them. A duplicate that makes it into reconciliation means you've probably already sent money somewhere twice. Two gates upstream meant reconciliation was always dealing with clean, unique payments."
Quick Reference — Pick By Question Type#
| Question Type | Best Trade-offs to Use |
|---|---|
| "Most challenging technical decision" | ClickHouse migration (2.1), 4-layer LLM pipeline (4.1), No retry on trade execution (5.2) |
| "Biggest trade-off you've made" | Monolith vs microservices (1.1), TheGraph vs own indexer (1.2), Metabase vs custom viz (6.1) |
| "Known limitation — how did you handle it" | Progressive disclosure (5.3), Reactive monitoring (3.4), Market cap tiering (2.3) |
| "Unreliable external APIs" | Prioritized fallback chain (3.1), Virtuals API monitoring (3.4) |
| "Scaling problem" | ClickHouse migration (2.1), Database coordinator (1.3), Market cap tiering (2.3) |
| "Chose simplicity over completeness" | Database coordinator (1.3), Polling vs SSE (5.1), Stablecoins only (3.2) |
| "AI-powered features — what went wrong" | 4-layer LLM pipeline (4.1), CSV vs JSON (4.2), AI Studio confidence threshold (4.3) |
| "Distributed systems coordination" | Database as coordinator (1.3), Rate limit distribution (3.3) |
| "Payment or financial system" | Poll-and-reconcile (7.1), Two-gate prevention (7.2), No retry on trades (5.2) |
| "Database selection" | ClickHouse evaluation (2.1), Self-hosted vs managed (1.4), ReplacingMergeTree fix (2.2) |
| "Build vs buy decision" | TheGraph vs own indexer (1.2), Metabase vs custom viz (6.1), Self-hosted ClickHouse (1.4) |
| "LLM design decisions" | Batched vs parallel LLM jobs (1.5), 4-layer pipeline (4.1), CSV vs JSON (4.2) |
Last updated April 2026