diff --git a/src/app/story/[storylineId]/page.tsx b/src/app/story/[storylineId]/page.tsx
index 54e36a81..47542dae 100644
--- a/src/app/story/[storylineId]/page.tsx
+++ b/src/app/story/[storylineId]/page.tsx
@@ -1,6 +1,7 @@
import { createServerClient, type Storyline, type Plot } from "../../../../lib/supabase";
import { DeadlineCountdown } from "../../../components/DeadlineCountdown";
import { TradingWidget } from "../../../components/TradingWidget";
+import { PriceChart } from "../../../components/PriceChart";
import { getTokenPrice, type TokenPriceInfo } from "../../../../lib/price";
import { IS_TESTNET } from "../../../../lib/contracts/constants";
import { type Address } from "viem";
@@ -53,6 +54,13 @@ export default async function StoryPage({ params }: { params: Params }) {
return (
+ {sl.token_address && priceInfo && (
+
+ )}
{sl.token_address && (
)}
diff --git a/src/components/PriceChart.tsx b/src/components/PriceChart.tsx
new file mode 100644
index 00000000..cf714444
--- /dev/null
+++ b/src/components/PriceChart.tsx
@@ -0,0 +1,206 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { type Address, parseUnits, formatUnits } from "viem";
+import { publicClient } from "../../lib/rpc";
+import { mcv2BondAbi } from "../../lib/price";
+import { MCV2_BOND, IS_TESTNET } from "../../lib/contracts/constants";
+
+const CHART_W = 320;
+const CHART_H = 140;
+const PAD = { top: 10, right: 10, bottom: 24, left: 48 };
+const PLOT_W = CHART_W - PAD.left - PAD.right;
+const PLOT_H = CHART_H - PAD.top - PAD.bottom;
+const NUM_POINTS = 20;
+
+interface PriceChartProps {
+ tokenAddress: Address;
+ totalSupplyRaw: bigint;
+ currentPriceRaw: bigint;
+}
+
+/**
+ * Lightweight bonding curve chart.
+ *
+ * Samples getReserveForToken at evenly spaced supply points to plot
+ * the price curve, then marks the current supply position.
+ */
+export function PriceChart({ tokenAddress, totalSupplyRaw, currentPriceRaw }: PriceChartProps) {
+ const reserveLabel = IS_TESTNET ? "WETH" : "$PLOT";
+
+ // Sample the bonding curve at multiple supply points
+ const { data: curvePoints } = useQuery({
+ queryKey: ["price-curve", tokenAddress],
+ queryFn: async () => {
+ // Sample from 1 token to 2x current supply (or a minimum of 100 tokens)
+ const minMax = parseUnits("100", 18);
+ const maxSupply =
+ totalSupplyRaw * BigInt(2) > minMax
+ ? totalSupplyRaw * BigInt(2)
+ : minMax;
+
+ const points: { supply: number; price: number }[] = [];
+ const step = maxSupply / BigInt(NUM_POINTS);
+ if (step === BigInt(0)) return [];
+
+ // Query cumulative costs for increasing mint amounts from current supply
+ const promises: Promise
[] = [];
+ for (let i = 1; i <= NUM_POINTS; i++) {
+ const amount = step * BigInt(i);
+ promises.push(
+ publicClient
+ .readContract({
+ address: MCV2_BOND,
+ abi: mcv2BondAbi,
+ functionName: "getReserveForToken",
+ args: [tokenAddress, amount],
+ })
+ .catch(() => BigInt(0)),
+ );
+ }
+
+ const cumulativeCosts = await Promise.all(promises);
+
+ // Start with the actual current supply/price point
+ const currentSupplyNum = Number(formatUnits(totalSupplyRaw, 18));
+ const currentPriceNum = Number(formatUnits(currentPriceRaw, 18));
+ points.push({ supply: currentSupplyNum, price: currentPriceNum });
+
+ // Compute marginal price at each future supply step
+ let prevCost = BigInt(0);
+ for (let i = 0; i < cumulativeCosts.length; i++) {
+ const amount = step * BigInt(i + 1);
+ const marginalCost = cumulativeCosts[i] - prevCost;
+ const pricePerToken =
+ Number(formatUnits(marginalCost, 18)) /
+ Number(formatUnits(step, 18));
+ points.push({
+ supply: currentSupplyNum + Number(formatUnits(amount, 18)),
+ price: pricePerToken,
+ });
+ prevCost = cumulativeCosts[i];
+ }
+ return points;
+ },
+ staleTime: 60000,
+ });
+
+ if (!curvePoints || curvePoints.length === 0) return null;
+
+ // Scale to chart coords
+ const maxX = Math.max(...curvePoints.map((p) => p.supply));
+ const maxY = Math.max(...curvePoints.map((p) => p.price));
+ if (maxX === 0 || maxY === 0) return null;
+
+ const scaleX = (v: number) => PAD.left + (v / maxX) * PLOT_W;
+ const scaleY = (v: number) => PAD.top + PLOT_H - (v / maxY) * PLOT_H;
+
+ // Build SVG polyline
+ const linePoints = curvePoints
+ .map((p) => `${scaleX(p.supply)},${scaleY(p.price)}`)
+ .join(" ");
+
+ // Current supply marker — uses actual current supply and price
+ const currentSupply = Number(formatUnits(totalSupplyRaw, 18));
+ const currentPrice = Number(formatUnits(currentPriceRaw, 18));
+ const markerX = scaleX(currentSupply);
+ const markerY = scaleY(currentPrice);
+
+ // Y-axis labels (3 ticks)
+ const yTicks = [0, maxY / 2, maxY];
+ // X-axis labels
+ const xTicks = [0, maxX / 2, maxX];
+
+ return (
+
+ Price Curve
+
+
+ Supply vs. price per token ({reserveLabel})
+ {currentSupply > 0 && (
+
+ {" "}
+ · current: {currentSupply.toLocaleString()} tokens
+
+ )}
+
+
+ );
+}