Repository navigation
feat(dapp): launch-readiness batch (closes #146) - #148
Merged
Merged
Conversation
Ten races a minute was a dev-testing number: every race now costs several paid model calls (DeepSeek, Claude, up to two OpenRouter seats), so it is lowered to four. A new 150/day cap, checked independently, bounds a caller who never trips the per-minute window but sustains it for hours — roughly $2 of worst-case Claude spend per key per day. compete was also IP-only: every other route is too, but compete is the one where a race costs real money per request, so it is the one that needed an account key most. The client now sends the connected wallet's address when there is one, peeked from a cloned request so the ordinary body parse is untouched, and the limiter checks it as well as the IP.
Not a second signature gate — the wallet's own prompt still owns that — but a forced pause on the one screen most likely to be misread in a hurry, for the mistake a wallet prompt does nothing to catch: a fat-fingered amount, a plan that looks fine at a glance. Threshold is the larger of what is spent and received, configurable via NEXT_PUBLIC_SWAP_CONFIRM_THRESHOLD_USD, kept deliberately separate from trade-cap.ts's MAINNET_MAX_TRADE_USD hard cap even though both default to $500 today — one is a server-side refusal, this is a client-side pause, and they should be free to move independently later.
A race was logged for aggregate stats, but the winning agent's own words were discarded before storage, and nothing linked a race to the wallet that acted on it. agent_proposals gets one new column, reasoning, written only on the row where won is true: a losing agent's prose was never acted on, so it was never worth keeping. The join to a wallet already existed — usage_executions .race_id, written from the x-intent-race header a submit already sends — so no new wallet column was needed. GET /api/agents/history reads a wallet's own races this way: address-only, no login, matching the trust model swap/history and the ledger already use for everything else this wallet did. The swap history page shows it inline on the swap that came from that race, next to the route and the explorer link.
Two real gaps in the swap-then-cashout chain, found by auditing all four partial-failure scenarios rather than assuming where the problems were. If a swap settled meaning to be cashed out and the tab closed before the form ever opened, nothing said so on return — the swap was already safe in history, but the fact that a second step was still owed was not. A local, per-wallet reminder now survives the closed tab and offers to reopen the form against the USDC that already arrived. A payout that failed or expired after the swap settled had no way to try again in either flow: NairaOrderTracker's standalone view had no retry at all, and the two existing retry paths (NairaPayoutCard's own edit(), sequence.retryPayout) were never wired to the one place both flows show a terminal status. PayoutProgress now takes an optional onRetry and both trackers pass theirs. Two scenarios audited and found already correct, not touched: an immediate cash-out-creation failure already preserves the settled swap's display, and the unconfirmed-swap race condition the cash-out step could have hit does not exist — the settle effect gates strictly on a real 'settled' phase.
Replaces running curl against production and reading Vercel logs by hand to notice a dead or rate-limited model. GET /api/agents/health reads the races already logged rather than running a synthetic race of its own, so it costs nothing beyond a couple of queries. A "core" agent (anything but OpenRouter, whose free models rotate and get benched on their own limits as normal operation) that answered zero times in the last 24h is critical; a roster-wide answer rate under 50% is a warning, zero is critical; fewer than 3 races in the window is reported as not enough to judge rather than guessed at. Authenticated with CRON_SECRET the way Vercel Cron actually calls a job — GET, Authorization: Bearer — not the POST and custom header standing/tick uses, which was never wired to a scheduler (no vercel.json existed before this). The account's plan is Hobby, which caps cron to once a day, so that is the cadence. An alert reuses the existing alerts_sent/sendAlert path the sponsor-balance alert already sends through, rather than a new, unconfigured webhook integration.
Resolves the two open risks the design doc flagged before any code was
written, both confirmed live against mainnet, not assumed from source:
- ask_amount is the real destination amount; total_return is the pre-fee
idealised one. ask_amount + commission_amount + spread_amount ==
total_return exactly, on two independent live swaps in opposite
directions. Using total_return would have overstated what a user
receives by the fee and the spread.
- Phoenix has no router to pin a static allowlist entry to. The quote
carries the exact pool address it priced, the builder signs only that
pool, and the post-signature re-check (which has no quote to compare
against) checks shape only — matching, not loosening, what Aquarius's
own re-check already does.
A live build-sign-simulate round trip against the real factory and a
real pool is in build-phoenix.live.test.ts, which is what caught two
real bugs before they shipped: Address.fromScVal doesn't decode a raw
ScAddress (fromScAddress does), and an Option<T> None is scvVoid, not a
nativeToScVal({type:'void'}) call.
Mainnet only - no testnet deployment exists to point at. Multihop
(routing through pairs with no direct pool) is out of scope: a
throwaway encoding attempt failed and a direct quoter needs nothing
from it.
Cropped to square and resized to the 128x128 webp every other venue logo uses. Phoenix's source was a wide icon+wordmark lockup with the mark at the left edge, not centered, so a left-aligned crop was used rather than the usual center crop -- a center crop on this one would have cut the mark off and kept mostly blank space and text. Linq's source was already square. No code change needed: SourceLogo and VenueCard both build the path from the venue id, and linq was already a listed venue with no logo.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements 7 of the 8 items from #146 in one PR, per the agreed scope. Item #3 (hardware wallet) is held out -- it needs a real-device spike before it's safe to ship, per the design research.
What's in this PR
Rate limiting --
competedown from 10/60s to 4/60s (every race now costs several paid model calls), plus a new 150/day cap checked independently. Also keys by wallet address, not just IP, when one is connected.Spend-limit confirmation -- a ~2s pause on the sign button for a trade worth $500+ (configurable via
NEXT_PUBLIC_SWAP_CONFIRM_THRESHOLD_USD), kept deliberately separate from the existingMAINNET_MAX_TRADE_USDhard cap.Agent audit trail -- a wallet can see why the agent whose plan it executed proposed what it did. One new DB column (
agent_proposals.reasoning, written only for the winning row), a newGET /api/agents/historyendpoint, surfaced inline in swap history.Error-recovery UX -- two real gaps found by auditing all four partial-failure scenarios in the swap-to-cashout chain: a reminder that survives a closed tab when a swap settled but the cash-out never started, and a working retry button on a payout that failed or expired (previously missing in the standalone tracker view).
Agent-health monitoring -- a daily cron (
GET /api/agents/health, Vercel Cron's actual auth shape) reads the races already logged and emails an alert when a core agent goes quiet or the roster-wide answer rate drops, replacing manually running curl against prod.Phoenix swap source -- a third mainnet AMM, added after live-verifying the mainnet factory, 13 pools, and the correct destination-amount field (
ask_amount, nottotal_return-- confirmed by matchingask_amount + commission_amount + spread_amount == total_returnon two real swaps) via an end-to-end build-sign-simulate test against the live contract.Dropped from #146's original scope: a second lending venue -- Templar turned out to be a NEAR Protocol product with no Soroban deployment, not Stellar.
Checks
Typecheck, lint and formatting all clean. 2,562 of 2,613 tests pass; the one failure (
offramp-anchors.live.test.ts) is a known-flaky network timing test that passes in isolation, unrelated to this branch.🤖 Generated with Claude Code