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
37 changes: 37 additions & 0 deletions src/app/api/config/gas-estimate/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ const DEFAULT_GAS_LIMIT = 450_000
const DEFAULT_GAS_USED = 180_000
const DEFAULT_SURPLUS_RATE = 0.0056
const DEFAULT_MILES_CALC_MAX_SLIPPAGE = 50
/** Mirrors `DEFAULT_SWEEP_OVERHEAD_FALLBACK` in route.ts and the backend's
* `costEstimateLastResort` (cost_estimator.go). */
const DEFAULT_SWEEP_OVERHEAD: Record<string, number> = { default: 0.001 }
/** Mirrors `DEFAULT_BID_COST_ETH` in route.ts — p75 of post-Apr-8 realized. */
const DEFAULT_BID_COST_ETH = 0.00004

/**
* Build a `mockGet` implementation that returns the values we want for each
Expand All @@ -40,15 +45,24 @@ describe("GET /api/config/gas-estimate", () => {
gasEstimate: DEFAULT_GAS_LIMIT,
gasUsedEstimate: DEFAULT_GAS_USED,
surplusRate: DEFAULT_SURPLUS_RATE,
sweepOverheadByToken: DEFAULT_SWEEP_OVERHEAD,
bidCostEth: DEFAULT_BID_COST_ETH,
milesCalcMaxSlippagePct: DEFAULT_MILES_CALC_MAX_SLIPPAGE,
})
})

it("passes through valid operator-set values", async () => {
const sweepMap = {
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": 0.00004,
"0xdac17f958d2ee523a2206206994597c13d831ec7": 0.00005,
default: 0.00008,
}
mockKeys({
miles_estimate_gas_limit_average: 500_000,
miles_estimate_gas_used_average: 200_000,
miles_estimate_surplus_rate: 0.012,
miles_estimate_sweep_overhead_eth_by_token: sweepMap,
miles_estimate_bid_cost_eth: 0.000038,
miles_calc_max_slippage_pct: 25,
})
const res = await GET()
Expand All @@ -58,10 +72,29 @@ describe("GET /api/config/gas-estimate", () => {
gasEstimate: 500_000,
gasUsedEstimate: 200_000,
surplusRate: 0.012,
sweepOverheadByToken: sweepMap,
bidCostEth: 0.000038,
milesCalcMaxSlippagePct: 25,
})
})

it("falls back to default sweep overhead when the map has a bad value", async () => {
mockKeys({
// negative overhead is nonsensical — the route should reject and fall back
miles_estimate_sweep_overhead_eth_by_token: { "0xfoo": -1 },
})
const res = await GET()
const json = await res.json()
expect(json.sweepOverheadByToken).toEqual(DEFAULT_SWEEP_OVERHEAD)
})

it("falls back to default sweep overhead when the map is non-object", async () => {
mockKeys({ miles_estimate_sweep_overhead_eth_by_token: "not a map" })
const res = await GET()
const json = await res.json()
expect(json.sweepOverheadByToken).toEqual(DEFAULT_SWEEP_OVERHEAD)
})

it("clamps milesCalcMaxSlippagePct above the 50% ceiling", async () => {
mockKeys({ miles_calc_max_slippage_pct: 75 })
const res = await GET()
Expand Down Expand Up @@ -116,6 +149,8 @@ describe("GET /api/config/gas-estimate", () => {
gasEstimate: DEFAULT_GAS_LIMIT,
gasUsedEstimate: DEFAULT_GAS_USED,
surplusRate: DEFAULT_SURPLUS_RATE,
sweepOverheadByToken: DEFAULT_SWEEP_OVERHEAD,
bidCostEth: DEFAULT_BID_COST_ETH,
milesCalcMaxSlippagePct: DEFAULT_MILES_CALC_MAX_SLIPPAGE,
})
})
Expand All @@ -127,9 +162,11 @@ describe("GET /api/config/gas-estimate", () => {
expect(fetchedKeys).toEqual(
[
"miles_calc_max_slippage_pct",
"miles_estimate_bid_cost_eth",
"miles_estimate_gas_limit_average",
"miles_estimate_gas_used_average",
"miles_estimate_surplus_rate",
"miles_estimate_sweep_overhead_eth_by_token",
].sort()
)
})
Expand Down
46 changes: 40 additions & 6 deletions src/app/api/config/gas-estimate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ export const runtime = "edge"
const DEFAULT_GAS_LIMIT = 450_000
const DEFAULT_GAS_USED = 180_000
const DEFAULT_SURPLUS_RATE = 0.0056
/**
* Cold-load fallback for the sweep-overhead map. Mirrors the backend's
* `costEstimateLastResort` in `fastswap-miles/cost_estimator.go`. Used
* only when Edge Config has no `miles_estimate_sweep_overhead_eth_by_token`
* entry at all (e.g. the hourly cron hasn't run for the first time yet);
* once populated, the cron writes a `default` key alongside per-token
* values and clients read that instead.
*/
const DEFAULT_SWEEP_OVERHEAD_FALLBACK: Record<string, number> = { default: 0.001 }
/**
* Cold-load fallback for the dashboard MilesCell bid-cost proxy. Same
* value the cron writes when no rows are sampled. Tracks p75 of realized
* bid_cost since 2026-04-08; tight enough post-fix that a single
* scalar is fine.
*/
const DEFAULT_BID_COST_ETH = 0.00004
/** Default upper bound the miles calculator will plan against, in percent. */
const DEFAULT_MILES_CALC_MAX_SLIPPAGE = 50
/** Hard floors and ceilings for the calc cap so a bad Edge Config value can't
Expand All @@ -19,21 +35,37 @@ function clampMaxSlippage(value: number): number {
return Math.min(MILES_CALC_MAX_SLIPPAGE_CEILING, Math.max(MILES_CALC_MAX_SLIPPAGE_FLOOR, value))
}

function isSweepOverheadMap(value: unknown): value is Record<string, number> {
if (value == null || typeof value !== "object") return false
for (const v of Object.values(value as Record<string, unknown>)) {
if (typeof v !== "number" || !Number.isFinite(v) || v < 0) return false
}
return true
}

export async function GET() {
try {
const [gasLimit, gasUsed, surplusRate, milesCalcMaxSlippage] = await Promise.all([
get<number>("miles_estimate_gas_limit_average"),
get<number>("miles_estimate_gas_used_average"),
get<number>("miles_estimate_surplus_rate"),
get<number>("miles_calc_max_slippage_pct"),
])
const [gasLimit, gasUsed, surplusRate, sweepOverheadByToken, bidCostEth, milesCalcMaxSlippage] =
await Promise.all([
get<number>("miles_estimate_gas_limit_average"),
get<number>("miles_estimate_gas_used_average"),
get<number>("miles_estimate_surplus_rate"),
get<Record<string, number>>("miles_estimate_sweep_overhead_eth_by_token"),
get<number>("miles_estimate_bid_cost_eth"),
get<number>("miles_calc_max_slippage_pct"),
])

return NextResponse.json(
{
gasEstimate: typeof gasLimit === "number" && gasLimit > 0 ? gasLimit : DEFAULT_GAS_LIMIT,
gasUsedEstimate: typeof gasUsed === "number" && gasUsed > 0 ? gasUsed : DEFAULT_GAS_USED,
surplusRate:
typeof surplusRate === "number" && surplusRate > 0 ? surplusRate : DEFAULT_SURPLUS_RATE,
sweepOverheadByToken: isSweepOverheadMap(sweepOverheadByToken)
? sweepOverheadByToken
: DEFAULT_SWEEP_OVERHEAD_FALLBACK,
bidCostEth:
typeof bidCostEth === "number" && bidCostEth > 0 ? bidCostEth : DEFAULT_BID_COST_ETH,
milesCalcMaxSlippagePct:
typeof milesCalcMaxSlippage === "number" && milesCalcMaxSlippage > 0
? clampMaxSlippage(milesCalcMaxSlippage)
Expand All @@ -48,6 +80,8 @@ export async function GET() {
gasEstimate: DEFAULT_GAS_LIMIT,
gasUsedEstimate: DEFAULT_GAS_USED,
surplusRate: DEFAULT_SURPLUS_RATE,
sweepOverheadByToken: DEFAULT_SWEEP_OVERHEAD_FALLBACK,
bidCostEth: DEFAULT_BID_COST_ETH,
milesCalcMaxSlippagePct: DEFAULT_MILES_CALC_MAX_SLIPPAGE,
},
{ headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300" } }
Expand Down
148 changes: 145 additions & 3 deletions src/app/api/cron/update-edge-config/miles-estimate-gas/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,31 @@ const FALLBACK_GAS_USED = 180_000
/** Fallback p25 surplus rate (0.56% from 2026-04-14 all-swap sample). */
const FALLBACK_SURPLUS_RATE = 0.0056

/**
* Mirrors the backend's `costEstimateLastResort` constant in
* `mev-commit/tools/fastswap-miles/cost_estimator.go`. Used as the JSON
* map's hardcoded `"default"` fallback when the cron can't compute a
* network-wide percentile (e.g. zero rows in the 14d window). The
* frontend hook reads this exact key out of the Edge Config map.
*/
const LAST_RESORT_SWEEP_OVERHEAD_ETH = 0.001

/**
* Minimum sample size before we trust per-token p25. Below this we fall
* back to that token's p75, exactly mirroring the backend's
* `costEstimateMinSweeps` constant. Keeps both estimators in lockstep
* even on low-volume tokens.
*/
const SWEEP_OVERHEAD_MIN_SAMPLES = 10

/**
* Fallback proxy for `bid_cost` on pending rows. Mirrors the hardcoded
* constant in `user-swaps-parts.tsx` and the post-2026-04-08 realized
* distribution's p75 — used when the analytics query can't produce a
* value (zero rows in window).
*/
const FALLBACK_BID_COST_ETH = 0.00004

// ---------------------------------------------------------------------------
// Auth helper
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -308,6 +333,109 @@ async function computeSurplusRateEstimate(): Promise<number> {
return rounded
}

// ---------------------------------------------------------------------------
// Bid cost p75
// ---------------------------------------------------------------------------

/**
* Computes the p75 of realized bid_cost (in ETH) across processed rows
* since the 2026-04-08 regime change, 30-day window. Used by the
* dashboard's MilesCell proxy in lieu of the historically hardcoded
* `ESTIMATED_BID_COST_ETH` constant. p75 to mirror the surplus-rate
* cron's under-promise philosophy.
*/
async function computeBidCostEstimate(): Promise<number> {
const client = getAnalyticsClient()

const rows = await client.execute("fastswap/get-bid-costs", {})
const values = rows
.map((row) => Number(row[0]))
.filter((v) => Number.isFinite(v) && v > 0)
.sort((a, b) => a - b)

if (values.length === 0) {
console.warn("[cron/miles-estimate-gas] No bid cost samples returned — using fallback")
return FALLBACK_BID_COST_ETH
}

const p75Index = Math.floor(values.length * 0.75)
const p75 = values[p75Index]
// Round to 8 decimal places (0.01 µETH) — same precision as sweep overhead.
const rounded = Math.round(p75 * 1e8) / 1e8

console.log(
`[cron/miles-estimate-gas] bidCost — count: ${values.length} ` +
`p25: ${(values[Math.floor(values.length * 0.25)] * 1e6).toFixed(2)} µETH ` +
`p50: ${(values[Math.floor(values.length * 0.5)] * 1e6).toFixed(2)} µETH ` +
`p75: ${(rounded * 1e6).toFixed(2)} µETH (chosen) ` +
`p95: ${(values[Math.floor(values.length * 0.95)] * 1e6).toFixed(2)} µETH`
)

return rounded
}

// ---------------------------------------------------------------------------
// Sweep overhead by token
// ---------------------------------------------------------------------------

/**
* Per-token sweep overhead map: lowercased L1 token address → ETH overhead.
*
* Mirrors the backend's `costEstimator.Get` semantics in
* `mev-commit/tools/fastswap-miles/cost_estimator.go`:
* - per-token p25 over the last 14 days when sample size ≥ 10
* - per-token p75 when sample size < 10 (low-data fallback)
* - `default` is a network-wide p25 across all tokens — used when the
* frontend's selected output token isn't in the map at all
* - falls back to `LAST_RESORT_SWEEP_OVERHEAD_ETH` if the network query
* itself returns nothing usable
*
* Returned values are ETH (float) so the route handler can ship the map
* straight to Edge Config without extra encoding.
*/
async function computeSweepOverheadByToken(): Promise<Record<string, number>> {
const client = getAnalyticsClient()

const rows = await client.execute("fastswap/get-sweep-overhead-by-token", {})

// `default` mirrors the backend's `costEstimateLastResort` exactly — for
// tokens the backend has no historical data on, both estimators must
// agree on the same fallback or the frontend will over-promise miles
// (frontend uses a small `default`, backend deducts 0.001 ETH) and the
// user gets fewer miles than the badge said.
const map: Record<string, number> = { default: LAST_RESORT_SWEEP_OVERHEAD_ETH }

for (const row of rows) {
const token = String(row[0] ?? "").toLowerCase()
const n = Number(row[1])
const p25 = Number(row[2])
const p75 = Number(row[3])

if (!token || token === "null") continue
if (!Number.isFinite(n) || n <= 0) continue
if (!Number.isFinite(p25) || !Number.isFinite(p75)) continue

const overhead = n >= SWEEP_OVERHEAD_MIN_SAMPLES ? p25 : p75
if (!Number.isFinite(overhead) || overhead < 0) continue

// Round to 8 decimal places (0.01 µETH) to keep Edge Config JSON tight.
map[token] = Math.round(overhead * 1e8) / 1e8
}

console.log(
`[cron/miles-estimate-gas] sweepOverhead — ${Object.keys(map).length - 1} tokens, default=${(map.default * 1e6).toFixed(0)} µETH (last-resort), sample=${JSON.stringify(
Object.fromEntries(
Object.entries(map)
.filter(([k]) => k !== "default")
.slice(0, 3)
.map(([k, v]) => [k.slice(0, 8) + "…", `${(v * 1e6).toFixed(1)}µETH`])
)
)}`
)

return map
}

// ---------------------------------------------------------------------------
// Route handler
// ---------------------------------------------------------------------------
Expand All @@ -327,7 +455,7 @@ async function computeSurplusRateEstimate(): Promise<number> {
* 4. Returns a JSON summary of the result.
*
* ### Schedule
* Configured in `vercel.json` to run daily at 00:00 UTC.
* Configured in `vercel.json` to run hourly at minute 0.
* Can also be triggered manually from the Vercel Dashboard → Cron Jobs tab.
*/
export async function GET(request: Request) {
Expand All @@ -339,14 +467,16 @@ export async function GET(request: Request) {

try {
// --- Step 2: Fetch the new values ----------------------------------------
const [gasAverages, surplusRate] = await Promise.all([
const [gasAverages, surplusRate, sweepOverheadByToken, bidCostEth] = await Promise.all([
computeGasAverages(),
computeSurplusRateEstimate(),
computeSweepOverheadByToken(),
computeBidCostEstimate(),
])
const { gasLimitAvg, gasUsedAvg } = gasAverages

console.log(
`[cron/miles-estimate-gas] Computed: gasLimit=${gasLimitAvg}, gasUsed=${gasUsedAvg}, surplusRate=${(surplusRate * 100).toFixed(2)}%`
`[cron/miles-estimate-gas] Computed: gasLimit=${gasLimitAvg}, gasUsed=${gasUsedAvg}, surplusRate=${(surplusRate * 100).toFixed(2)}%, bidCostEth=${bidCostEth}, sweepOverheadTokens=${Object.keys(sweepOverheadByToken).length - 1} (default=${sweepOverheadByToken.default})`
)

// --- Step 3: Write to Edge Config ---------------------------------------
Expand All @@ -370,6 +500,16 @@ export async function GET(request: Request) {
key: "miles_estimate_surplus_rate",
value: surplusRate,
},
{
operation: "upsert",
key: "miles_estimate_sweep_overhead_eth_by_token",
value: sweepOverheadByToken,
},
{
operation: "upsert",
key: "miles_estimate_bid_cost_eth",
value: bidCostEth,
},
])

console.log(
Expand All @@ -384,6 +524,8 @@ export async function GET(request: Request) {
gasLimitAverage: gasLimitAvg,
gasUsedAverage: gasUsedAvg,
surplusRate,
sweepOverheadByToken,
bidCostEth,
},
vercelResponse: result,
})
Expand Down
6 changes: 3 additions & 3 deletions src/components/dashboard/UserSwapsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "@/components/ui/dialog"
import { Skeleton } from "@/components/ui/skeleton"
import { useUserSwaps } from "@/hooks/use-user-swaps"
import { useSurplusRate } from "@/hooks/use-surplus-rate"
import { useMilesEstimateConfig } from "@/hooks/use-miles-estimate-config"
import { SwapsTableBody } from "./user-swaps-parts"

type Props = {
Expand Down Expand Up @@ -82,7 +82,7 @@ function UserSwapsModalBody({ address }: { address: string }) {
page,
pageSize: MODAL_PAGE_SIZE,
})
const surplusRate = useSurplusRate()
const { surplusRate, bidCostEth } = useMilesEstimateConfig()

const { totalPages, total } = pagination

Expand Down Expand Up @@ -123,7 +123,7 @@ function UserSwapsModalBody({ address }: { address: string }) {
No Fast Swaps yet. Make your first swap to start earning miles.
</div>
) : (
<SwapsTableBody swaps={swaps} surplusRate={surplusRate} />
<SwapsTableBody swaps={swaps} surplusRate={surplusRate} bidCostEth={bidCostEth} />
)}
</div>

Expand Down
Loading
Loading