export const prerender = false; import type { APIRoute } from "astro"; import { and, eq, sql } from "drizzle-orm"; import { createDb } from "@/db"; import { markets, positions, profiles, trades } from "@/db/schema"; import { authenticate } from "@/lib/auth"; export const POST: APIRoute = async ({ request, locals }) => { const runtime = locals.runtime as { env: { DATABASE_URL: string } }; const db = createDb(runtime.env.DATABASE_URL); const profile = await authenticate(request, db); if (!profile) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const ct = request.headers.get("Content-Type")?.split(";")[0].trim(); if (ct !== "application/json") { return Response.json({ error: "Content-Type must be application/json" }, { status: 415 }); } let body: { marketSlug?: string; side?: string; action?: string; amount?: number }; try { body = await request.json(); } catch { return Response.json({ error: "Invalid JSON body" }, { status: 400 }); } const { marketSlug, side, action, amount } = body; if (!marketSlug || typeof marketSlug !== "string") { return Response.json({ error: "Missing marketSlug" }, { status: 400 }); } if (side !== "yes" && side !== "no") { return Response.json({ error: "side must be 'yes' or 'no'" }, { status: 400 }); } if (action !== "buy" && action !== "sell") { return Response.json({ error: "action must be 'buy' or 'sell'" }, { status: 400 }); } if (typeof amount !== "number" || amount <= 0 || !Number.isFinite(amount)) { return Response.json({ error: "amount must be a positive number" }, { status: 400 }); } const result = await db.transaction(async (tx) => { const [market] = await tx .select({ id: markets.id, poolYes: markets.poolYes, poolNo: markets.poolNo, active: markets.active, }) .from(markets) .where(eq(markets.slug, marketSlug)) .for("update"); if (!market || !market.active) { return { error: "Market not found or inactive" as const, _status: 404 }; } const [freshProfile] = await tx .select({ balance: profiles.balance }) .from(profiles) .where(eq(profiles.id, profile.id)) .for("update"); const poolYes = parseFloat(market.poolYes); const poolNo = parseFloat(market.poolNo); const k = poolYes * poolNo; const balance = parseFloat(freshProfile.balance); if (action === "buy") { const cost = side === "yes" ? poolYes - k / (poolNo + amount) : poolNo - k / (poolYes + amount); if (cost <= 0) { return { error: "Invalid trade amount" as const, _status: 400 }; } if (cost > balance) { return { error: "Insufficient balance" as const, required: cost, available: balance, _status: 400 }; } const price = cost / amount; const newPoolYes = side === "yes" ? poolYes - cost : poolYes; const newPoolNo = side === "no" ? poolNo - cost : poolNo; const addToPool = side === "yes" ? poolNo + amount : poolYes + amount; const finalPoolYes = side === "yes" ? newPoolYes : addToPool; const finalPoolNo = side === "no" ? newPoolNo : addToPool; await tx .update(profiles) .set({ balance: sql`${profiles.balance}::numeric - ${cost.toFixed(4)}::numeric` }) .where(eq(profiles.id, profile.id)); await tx .update(markets) .set({ poolYes: finalPoolYes.toFixed(4), poolNo: finalPoolNo.toFixed(4), priceYes: (finalPoolNo / (finalPoolYes + finalPoolNo)).toFixed(4), volume: sql`${markets.volume}::numeric + ${cost.toFixed(4)}::numeric`, updatedAt: new Date(), }) .where(eq(markets.id, market.id)); const [existingPos] = await tx .select({ id: positions.id, shares: positions.shares, avgPrice: positions.avgPrice }) .from(positions) .where(and(eq(positions.profileId, profile.id), eq(positions.marketId, market.id), eq(positions.side, side))); if (existingPos) { const oldShares = parseFloat(existingPos.shares); const oldAvg = parseFloat(existingPos.avgPrice); const newShares = oldShares + amount; const newAvg = (oldAvg * oldShares + price * amount) / newShares; await tx .update(positions) .set({ shares: newShares.toFixed(4), avgPrice: newAvg.toFixed(4), updatedAt: new Date() }) .where(eq(positions.id, existingPos.id)); } else { await tx.insert(positions).values({ profileId: profile.id, marketId: market.id, side, shares: amount.toFixed(4), avgPrice: price.toFixed(4), }); } const [trade] = await tx .insert(trades) .values({ profileId: profile.id, marketId: market.id, action: "buy", side, shares: amount.toFixed(4), price: price.toFixed(4), amount: cost.toFixed(4), fee: "0.0000", poolYesAfter: finalPoolYes.toFixed(4), poolNoAfter: finalPoolNo.toFixed(4), }) .returning({ id: trades.id }); return { tradeId: trade.id, cost, price, shares: amount, poolYes: finalPoolYes, poolNo: finalPoolNo }; } const [existingPos] = await tx .select({ id: positions.id, shares: positions.shares, avgPrice: positions.avgPrice }) .from(positions) .where(and(eq(positions.profileId, profile.id), eq(positions.marketId, market.id), eq(positions.side, side))) .for("update"); if (!existingPos || parseFloat(existingPos.shares) < amount) { return { error: "Insufficient shares" as const, available: existingPos ? parseFloat(existingPos.shares) : 0, _status: 400, }; } const payout = side === "yes" ? poolNo - k / (poolYes + amount) : poolYes - k / (poolNo + amount); if (payout <= 0) { return { error: "Invalid trade amount" as const, _status: 400 }; } const price = payout / amount; const finalPoolYes = side === "yes" ? poolYes + amount : poolYes - payout; const finalPoolNo = side === "no" ? poolNo + amount : poolNo - payout; const avgPrice = parseFloat(existingPos.avgPrice); const realizedPnl = (price - avgPrice) * amount; await tx .update(profiles) .set({ balance: sql`${profiles.balance}::numeric + ${payout.toFixed(4)}::numeric` }) .where(eq(profiles.id, profile.id)); await tx .update(markets) .set({ poolYes: finalPoolYes.toFixed(4), poolNo: finalPoolNo.toFixed(4), priceYes: (finalPoolNo / (finalPoolYes + finalPoolNo)).toFixed(4), volume: sql`${markets.volume}::numeric + ${payout.toFixed(4)}::numeric`, updatedAt: new Date(), }) .where(eq(markets.id, market.id)); const newShares = parseFloat(existingPos.shares) - amount; await tx .update(positions) .set({ shares: newShares.toFixed(4), realizedPnl: sql`${positions.realizedPnl}::numeric + ${realizedPnl.toFixed(4)}::numeric`, updatedAt: new Date(), }) .where(eq(positions.id, existingPos.id)); const [trade] = await tx .insert(trades) .values({ profileId: profile.id, marketId: market.id, action: "sell", side, shares: amount.toFixed(4), price: price.toFixed(4), amount: payout.toFixed(4), fee: "0.0000", poolYesAfter: finalPoolYes.toFixed(4), poolNoAfter: finalPoolNo.toFixed(4), }) .returning({ id: trades.id }); return { tradeId: trade.id, payout, price, shares: amount, realizedPnl, poolYes: finalPoolYes, poolNo: finalPoolNo, }; }); if ("error" in result) { const { _status, ...body } = result; return Response.json(body, { status: _status }); } return Response.json(result); };