Skip to content

feat(dapp): launch-readiness batch (closes #146) - #148

Merged
Depo-dev merged 7 commits into
Cookit-labs:mainfrom
Depo-dev:feat/launch-readiness-batch
Oct 28, 2026
Merged

Depo-dev merged 7 commits into
Cookit-labs:mainfrom
Depo-dev:feat/launch-readiness-batch

Conversation

@Depo-dev

@Depo-dev Depo-dev commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

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 -- compete down 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 existing MAINNET_MAX_TRADE_USD hard 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 new GET /api/agents/history endpoint, 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, not total_return -- confirmed by matching ask_amount + commission_amount + spread_amount == total_return on 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

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.
@Depo-dev Depo-dev changed the title Feat/launch readiness batch feat(dapp): launch-readiness batch (closes #146) Oct 28, 2026
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.
@Depo-dev
Depo-dev merged commit 86cde42 into Cookit-labs:main Oct 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant