Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions content/learn/miles.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,30 @@ This means your miles for small ERC-20 output swaps may appear later — potenti
- **Swap size matters**: Very small swaps (especially to ERC-20 tokens) may not generate enough mev for immediate miles crediting.
- **ETH output is faster**: Swaps with ETH as the output token don't require a token sweep, so miles process more quickly.

## About the miles estimate

Before you swap, Fast Protocol shows an estimated miles amount in the swap interface. This estimate is a **conservative lower bound**, not a guarantee or a ceiling.

### Why the estimate can show "TBD"

The miles an individual swap earns depends on factors the UI can't know in advance — market conditions at execution time, the mev opportunity your swap creates, and gas costs. Because the estimate runs before any of that is known, the UI deliberately errs on the low side to avoid over-promising.

For some swaps, the conservative calculation can't confidently predict a number, so the UI shows **TBD** (to be determined). **This does not mean the swap will earn zero miles.** In practice, many swaps that show TBD go on to earn miles once the swap settles on-chain.

### What to trust instead

- The **estimate** is a directional signal. Treat it as a floor, not a forecast.
- The **actual miles earned** are computed from real on-chain activity after the swap settles.
- Your **dashboard swap history** shows the finalized miles for each transaction once processing completes. This is the authoritative number.

If you see TBD or a low estimate and still want to swap, go ahead — the real number is computed post-settlement and may be higher.

### Why not just show a bigger estimate?

Over-predicting miles would be worse than under-predicting. If the estimate consistently showed more miles than users actually earned, trust would collapse. The conservative approach means users are sometimes pleasantly surprised, and never disappointed by inflated predictions.

As we gather more data from real swaps, the estimator becomes more accurate. The tradeoff is intentional: accuracy improves with volume, and the cost of being wrong is paid in caution rather than exaggeration.

## Miles and mev rewards

Miles work alongside [mev rewards](/learn/mev-rewards) — they're complementary systems, not alternatives.
Expand Down
25 changes: 24 additions & 1 deletion src/app/(app)/SwapOrLandingGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { useAccount } from "wagmi"
import { FEATURE_FLAGS } from "@/lib/feature-flags"
import { useGateStatus } from "@/hooks/use-gate-status"
import { useGateStatus, getCachedApproval } from "@/hooks/use-gate-status"
import { useGateView } from "./GateViewContext"
import { Hero } from "@/components/swap/HeroSection"
import { AnimatedBackgroundOrbs } from "@/components/swap/OrbAnimatedBackground"
Expand Down Expand Up @@ -41,13 +41,28 @@ export function SwapOrLandingGate() {
const [view, setView] = useState<GateView>("landing")
// True when user clicked the button while disconnected — auto-proceeds after wallet connects
const [earlyAccessIntended, setEarlyAccessIntended] = useState(false)
// True once the initial sessionStorage check has run. Until then we
// suppress all rendering so the landing page never flashes for
// returning approved users.
const [ready, setReady] = useState(false)

// Helper: transition to swap and tell the layout to show the app header
const goToSwap = () => {
enterSwap()
setView("swap")
}

// On mount, check sessionStorage BEFORE rendering any content.
// If the user has a cached approval, jump straight to swap on the
// very first post-mount render — no landing page flash, no API call.
useEffect(() => {
if (address && getCachedApproval(address)) {
goToSwap()
}
setReady(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

// After connecting + checks resolve, proceed automatically if user had clicked the button
useEffect(() => {
if (!earlyAccessIntended || !isConnected || isCheckingAccess) return
Expand Down Expand Up @@ -99,6 +114,14 @@ export function SwapOrLandingGate() {
return <SwapContent />
}

// Suppress all rendering until the mount effect has checked
// sessionStorage. This is a single invisible frame (~16ms) that
// prevents the landing page from flashing for returning users
// who already have a cached approval.
if (!ready) {
return null
}

if (view === "swap") {
return <SwapContent />
}
Expand Down
13 changes: 13 additions & 0 deletions src/app/(app)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,19 @@ import { OneTimeTasksSection } from "@/components/dashboard/OneTimeTasksSection"
import { SBTGatingModal } from "@/components/modals/SBTGatingModal"
import { TransactionFeedbackModal } from "@/components/modals/TransactionFeedbackModal"
import { ReferralModal } from "@/components/modals/ReferralModal"
import dynamic from "next/dynamic"
import { EcosystemSetCarousel } from "@/components/dashboard/EcosystemSetsCarousel"

// Client-only: UserSwapsTable depends on wagmi state (address, isConnected)
// that differs between server and client. Rendering it on the server produces
// HTML that the client immediately disagrees with, causing a cascading hydration
// error that unmounts the component entirely. `ssr: false` avoids the issue by
// deferring the first render to the client — no server HTML, no mismatch.
const UserSwapsTable = dynamic(
() => import("@/components/dashboard/UserSwapsTable").then((m) => m.UserSwapsTable),
{ ssr: false }
)

import type { TaskName } from "@/hooks/use-dashboard-tasks"

// Dashboard page content - uses shared (app) layout for header and RPC/network modals
Expand Down Expand Up @@ -205,6 +216,8 @@ const DashboardContent = () => {
</div>
</div>

<UserSwapsTable address={address} isConnected={isConnected} />

<EcosystemSetCarousel />

<OneTimeTasksAccordion
Expand Down
15 changes: 10 additions & 5 deletions src/app/api/config/gas-estimate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@ import { get } from "@vercel/edge-config"

export const runtime = "edge"

const DEFAULT_GAS_ESTIMATE = 450_000
const DEFAULT_GAS_LIMIT = 450_000
const DEFAULT_GAS_USED = 180_000

export async function GET() {
try {
const gasEstimate = await get<number>("miles_estimate_gas_limit_average")
const [gasLimit, gasUsed] = await Promise.all([
get<number>("miles_estimate_gas_limit_average"),
get<number>("miles_estimate_gas_used_average"),
])

return NextResponse.json(
{
gasEstimate:
typeof gasEstimate === "number" && gasEstimate > 0 ? gasEstimate : DEFAULT_GAS_ESTIMATE,
gasEstimate: typeof gasLimit === "number" && gasLimit > 0 ? gasLimit : DEFAULT_GAS_LIMIT,
gasUsedEstimate: typeof gasUsed === "number" && gasUsed > 0 ? gasUsed : DEFAULT_GAS_USED,
},
{ headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300" } }
)
} catch (error) {
console.error("[gas-estimate] Edge Config read failed:", error)
return NextResponse.json(
{ gasEstimate: DEFAULT_GAS_ESTIMATE },
{ gasEstimate: DEFAULT_GAS_LIMIT, gasUsedEstimate: DEFAULT_GAS_USED },
{ headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300" } }
)
}
Expand Down
Loading
Loading