Skip to content
Open
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
54 changes: 54 additions & 0 deletions api/[...path].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Single catch-all serverless function for the entire OG DEX API.
*
* Vercel's Hobby plan caps a deployment at 12 serverless functions, and each
* file in /api normally becomes its own function. To stay under the limit (and
* keep adding endpoints freely), every route lives in api/_routes/*.js — the
* leading underscore folder is excluded from Vercel's function count — and this
* one function dispatches to them by the first path segment.
*
* Frontend URLs are unchanged: /api/wallet, /api/launch?config=1, etc.
* To add a new endpoint: drop api/_routes/<name>.js and register it below.
*/
import admin from "./_routes/admin.js";
import boosts from "./_routes/boosts.js";
import chart from "./_routes/chart.js";
import config from "./_routes/config.js";
import kols from "./_routes/kols.js";
import launch from "./_routes/launch.js";
import launches from "./_routes/launches.js";
import listings from "./_routes/listings.js";
import report from "./_routes/report.js";
import screener from "./_routes/screener.js";
import search from "./_routes/search.js";
import token from "./_routes/token.js";
import track from "./_routes/track.js";
import wallet from "./_routes/wallet.js";

const ROUTES = {
admin, boosts, chart, config, kols, launch, launches,
listings, report, screener, search, token, track, wallet,
};

export default async function handler(req, res) {
// Resolve the route segment from the path (e.g. /api/wallet -> "wallet").
// Fall back to Vercel's parsed catch-all param if present.
let seg = "";
try {
const { pathname } = new URL(req.url, "http://x");
seg = pathname.replace(/^\/+/, "").replace(/^api\//, "").split("/")[0].split("?")[0];
} catch {}
if (!seg && req.query) {
const p = req.query.path;
seg = Array.isArray(p) ? p[0] : (p || "");
}

const route = ROUTES[seg];
if (!route) {
res.statusCode = 404;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ ok: false, error: `unknown route: ${seg || "(none)"}` }));
return;
}
return route(req, res);
}
2 changes: 1 addition & 1 deletion api/admin.js → api/_routes/admin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { send, dbSelect, dbUpdate, dbDelete, dbInsert, readBody, ADMIN_PASS } from "./_lib.js";
import { send, dbSelect, dbUpdate, dbDelete, dbInsert, readBody, ADMIN_PASS } from "../_lib.js";

function auth(pass) { return pass && String(pass) === String(ADMIN_PASS); }

Expand Down
2 changes: 1 addition & 1 deletion api/boosts.js → api/_routes/boosts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { send, cache, dbSelect, dbInsert, dbUpdate, readBody, PAY_WALLET } from "./_lib.js";
import { send, cache, dbSelect, dbInsert, dbUpdate, readBody, PAY_WALLET } from "../_lib.js";

// Boost tiers
const TIERS = [
Expand Down
2 changes: 1 addition & 1 deletion api/chart.js → api/_routes/chart.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { send, cache } from "./_lib.js";
import { send, cache } from "../_lib.js";

// OHLCV candles for the token's top liquidity pool, via GeckoTerminal (free, no key).
// Used by the price chart on the coin page. Returns ascending candles for lightweight-charts.
Expand Down
2 changes: 1 addition & 1 deletion api/config.js → api/_routes/config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { send, PAY_WALLET, cache } from "./_lib.js";
import { send, PAY_WALLET, cache } from "../_lib.js";
export default async function handler(req, res) {
cache(res, 300, 600);
return send(res, 200, {
Expand Down
10 changes: 5 additions & 5 deletions api/kols.js → api/_routes/kols.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { callFn, send, cache, dbSelect, dbInsert, dbUpdate, readBody, ADMIN_PASS } from "./_lib.js";
import { parseSwap } from "./_swap.js";
import { enrichTokens } from "./_market.js";
import { computePnl } from "./_pnl.js";
import { callFn, send, cache, dbSelect, dbInsert, dbUpdate, readBody, ADMIN_PASS } from "../_lib.js";
import { parseSwap } from "../_swap.js";
import { enrichTokens } from "../_market.js";
import { computePnl } from "../_pnl.js";
import { readFileSync } from "fs";

const SOL = "So11111111111111111111111111111111111111112";
let SEED = { kols: [] };
try { SEED = JSON.parse(readFileSync(new URL("./_kols.json", import.meta.url), "utf8")); } catch {}
try { SEED = JSON.parse(readFileSync(new URL("../_kols.json", import.meta.url), "utf8")); } catch {}

const pub = (p) => ({
kolId: p.kol_id || p.id || null, name: p.name, twitter: p.x_handle || null,
Expand Down
219 changes: 219 additions & 0 deletions api/_routes/launch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { send, callFn, dbInsert, dbSelect, readBody, PAY_WALLET } from "../_lib.js";

/**
* OG DEX Token Launcher backend.
*
* Flat $5 launch fee, paid in SOL or USDC/USDT to PAY_WALLET (same wallet as
* boosts/listings), verified on-chain via the fee transaction signature.
*
* GET /api/launch?config=1
* → { ok, feeUsd, payWallet, solPrice, usdcMint, usdtMint }
*
* POST /api/launch (body.step)
* step "ipfs" { imageBase64, imageMimeType, name, symbol, description, twitter, telegram, website }
* → { metadataUri, metadata } (uploads to pump.fun IPFS)
* step "create" { publicKey, metadataUri, name, symbol, mintPublicKey, devBuySol, slippage }
* → { transaction } (unsigned PumpPortal create tx, base64)
* step "record" { payment_tx, pay_currency, creator_wallet, mint, name, symbol, icon,
* description, launch_tx, links }
* → { ok, token } verifies the $5 fee on-chain, then stores the launch
*
* Launched tokens are stored UNVERIFIED with no boost — they surface only in
* the "Newly Listed" section (/api/launches).
*/

const FEE_USD = 5;
const FEE_TOLERANCE = 0.92; // accept >= 92% of $5 to absorb price drift
const SOL_MINT = "So11111111111111111111111111111111111111112";
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const USDT_MINT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
const STABLES = { [USDC_MINT]: "usdc", [USDT_MINT]: "usdt" };

async function rpc(method, params) {
const r = await callFn("rpc-proxy", { jsonrpc: "2.0", id: 1, method, params });
return r?.data?.result ?? r?.result ?? null;
}

async function solPriceUsd() {
try {
const r = await fetch(`https://lite-api.jup.ag/price/v2?ids=${SOL_MINT}`);
const d = await r.json();
const p = Number(d?.data?.[SOL_MINT]?.price);
return p > 0 ? p : null;
} catch { return null; }
}

export default async function handler(req, res) {
if (req.method === "GET") {
const url = new URL(req.url, "http://x");
if (url.searchParams.get("config")) {
const solPrice = await solPriceUsd();
return send(res, 200, {
ok: true, feeUsd: FEE_USD, payWallet: PAY_WALLET, solPrice,
usdcMint: USDC_MINT, usdtMint: USDT_MINT, solMint: SOL_MINT,
});
}
return send(res, 400, { ok: false, error: "use ?config=1 or POST" });
}
if (req.method !== "POST") return send(res, 405, { ok: false, error: "method not allowed" });

try {
const body = await readBody(req);
switch (body?.step) {
case "ipfs": return await handleIpfs(body, res);
case "create": return await handleCreate(body, res);
case "record": return await handleRecord(body, res);
default: return send(res, 400, { ok: false, error: "invalid step (ipfs|create|record)" });
}
} catch (e) {
return send(res, 500, { ok: false, error: String(e?.message || e) });
}
}

/* ── Step 1: upload image + metadata to pump.fun IPFS ─────────────────── */
async function handleIpfs(body, res) {
const { imageBase64, imageMimeType, name, symbol, description, twitter, telegram, website } = body;
if (!imageBase64 || !name || !symbol)
return send(res, 400, { ok: false, error: "imageBase64, name and symbol are required" });

const imageBuffer = Buffer.from(imageBase64, "base64");
const ext = (imageMimeType || "image/png").split("/")[1] || "png";
const form = new FormData();
form.append("file", new Blob([imageBuffer], { type: imageMimeType || "image/png" }), `token.${ext}`);
form.append("name", name);
form.append("symbol", symbol);
form.append("description", description || "");
form.append("twitter", twitter || "");
form.append("telegram", telegram || "");
form.append("website", website || "");
form.append("showName", "true");

const r = await fetch("https://pump.fun/api/ipfs", { method: "POST", body: form });
if (!r.ok) return send(res, 502, { ok: false, error: `IPFS upload failed (${r.status}): ${await r.text()}` });
const data = await r.json();
if (!data.metadataUri) return send(res, 502, { ok: false, error: "no metadataUri returned" });
return send(res, 200, { ok: true, metadataUri: data.metadataUri, metadata: data.metadata || data });
}

/* ── Step 2: build the unsigned create transaction via PumpPortal ──────── */
async function handleCreate(body, res) {
const { publicKey, metadataUri, name, symbol, mintPublicKey, devBuySol, slippage } = body;
if (!publicKey || !metadataUri || !name || !symbol || !mintPublicKey)
return send(res, 400, { ok: false, error: "missing required fields" });

const payload = {
publicKey, action: "create",
tokenMetadata: { name, symbol, uri: metadataUri },
mint: mintPublicKey,
denominatedInSol: "true",
amount: Number(devBuySol) || 0,
slippage: Number(slippage) || 10,
priorityFee: 0.0005,
pool: "pump",
};
const r = await fetch("https://pumpportal.fun/api/trade-local", {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload),
});
if (!r.ok) return send(res, 502, { ok: false, error: `PumpPortal create failed (${r.status}): ${await r.text()}` });
const txBase64 = Buffer.from(new Uint8Array(await r.arrayBuffer())).toString("base64");
return send(res, 200, { ok: true, transaction: txBase64 });
}

/* ── Step 3: verify the $5 fee on-chain, then record the launch ────────── */
async function handleRecord(body, res) {
const mint = String(body.mint || "").trim();
const payment_tx = String(body.payment_tx || "").trim();
const pay_currency = String(body.pay_currency || "sol").toLowerCase();
if (!mint) return send(res, 400, { ok: false, error: "mint required" });
if (!payment_tx) return send(res, 400, { ok: false, error: "payment_tx required" });

// Reject reused payments (defence in depth — DB has a UNIQUE constraint too).
try {
const dup = await dbSelect("ogdex_launches", `payment_tx=eq.${encodeURIComponent(payment_tx)}&select=id&limit=1`);
if (dup.length) return send(res, 409, { ok: false, error: "this payment has already been used" });
} catch {}

const verify = await verifyFee(payment_tx, pay_currency);
if (!verify.ok) return send(res, 402, { ok: false, error: verify.error || "fee payment could not be verified" });

const row = {
mint,
symbol: body.symbol || null,
name: body.name || null,
icon: body.icon || null,
description: body.description || null,
creator_wallet: body.creator_wallet || null,
pay_currency: ["sol", "usdc", "usdt"].includes(pay_currency) ? pay_currency : "sol",
fee_usd: FEE_USD,
payment_tx,
launch_tx: body.launch_tx || null,
links: body.links || {},
status: "listed",
};
let inserted;
try {
inserted = await dbInsert("ogdex_launches", row);
} catch (e) {
if (String(e?.message || e).includes("duplicate"))
return send(res, 409, { ok: false, error: "this payment or token has already been recorded" });
throw e;
}
const token = (inserted && inserted[0]) || row;
return send(res, 200, {
ok: true,
token: { ...token, paidUsd: verify.usd },
links: {
pumpfun: `https://pump.fun/${mint}`,
solscan: `https://solscan.io/token/${mint}`,
ogdex: `/token/${mint}`,
},
});
}

/**
* Verify a fee payment transaction actually transferred >= $5 (allowing
* small price drift) to PAY_WALLET, in SOL or the chosen stablecoin.
*/
async function verifyFee(signature, payCurrency) {
let tx;
try {
tx = await rpc("getTransaction", [signature, { encoding: "jsonParsed", maxSupportedTransactionVersion: 0 }]);
} catch (e) {
return { ok: false, error: `could not fetch transaction: ${String(e?.message || e)}` };
}
if (!tx) return { ok: false, error: "transaction not found yet — wait for confirmation and retry" };
if (tx.meta?.err) return { ok: false, error: "fee transaction failed on-chain" };

const min = FEE_USD * FEE_TOLERANCE;

if (payCurrency === "sol") {
const keys = (tx.transaction?.message?.accountKeys || []).map((k) => (typeof k === "string" ? k : k.pubkey));
const idx = keys.indexOf(PAY_WALLET);
if (idx < 0) return { ok: false, error: "payment wallet not found in transaction" };
const pre = tx.meta?.preBalances?.[idx] ?? 0;
const post = tx.meta?.postBalances?.[idx] ?? 0;
const lamports = post - pre;
if (lamports <= 0) return { ok: false, error: "no SOL received by payment wallet" };
const price = await solPriceUsd();
if (!price) return { ok: false, error: "could not fetch SOL price" };
const usd = (lamports / 1e9) * price;
if (usd < min) return { ok: false, error: `fee too low: received ~$${usd.toFixed(2)}, need $${FEE_USD}` };
return { ok: true, usd: Number(usd.toFixed(2)), currency: "sol" };
}

// Stablecoin (USDC / USDT): compare token balance deltas owned by PAY_WALLET.
const pre = tx.meta?.preTokenBalances || [];
const post = tx.meta?.postTokenBalances || [];
let best = 0;
for (const pb of post) {
if (pb.owner !== PAY_WALLET) continue;
if (!STABLES[pb.mint]) continue;
const match = pre.find((x) => x.accountIndex === pb.accountIndex) || {};
const before = Number(match.uiTokenAmount?.uiAmount || 0);
const after = Number(pb.uiTokenAmount?.uiAmount || 0);
best = Math.max(best, after - before);
}
if (best <= 0) return { ok: false, error: "no USDC/USDT received by payment wallet" };
if (best < min) return { ok: false, error: `fee too low: received ~$${best.toFixed(2)}, need $${FEE_USD}` };
return { ok: true, usd: Number(best.toFixed(2)), currency: payCurrency };
}
49 changes: 49 additions & 0 deletions api/_routes/launches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { send, dbSelect, cache } from "../_lib.js";
import { enrichTokens } from "../_market.js";

/**
* GET /api/launches → tokens launched through OG DEX ("Newly Listed").
* These are UNVERIFIED and carry no boost. Live price/mcap are enriched
* on read so the section stays current.
*
* Query: ?limit=50
*/
export default async function handler(req, res) {
const url = new URL(req.url, "http://x");
const limit = Math.min(Number(url.searchParams.get("limit")) || 50, 200);
cache(res, 15, 60);
try {
const rows = await dbSelect(
"ogdex_launches",
`status=eq.listed&order=created_at.desc&limit=${limit}`
);
let live = {};
try { live = await enrichTokens(rows.map((r) => r.mint)); } catch {}
const out = rows.map((r) => {
const m = live[r.mint] || {};
return {
mint: r.mint,
symbol: r.symbol || m.symbol || null,
name: r.name || m.name || null,
icon: r.icon || m.image || null,
description: r.description || null,
creator_wallet: r.creator_wallet || null,
created_at: r.created_at,
launch_tx: r.launch_tx || null,
priceUsd: m.price ?? null,
mcap: m.mcap ?? null,
verified: false,
boosted: false,
source: "ogdex-launch",
links: {
pumpfun: `https://pump.fun/${r.mint}`,
solscan: `https://solscan.io/token/${r.mint}`,
ogdex: `/token/${r.mint}`,
},
};
});
return send(res, 200, { ok: true, count: out.length, rows: out });
} catch (e) {
return send(res, 200, { ok: true, rows: [], error: String(e?.message || e) });
}
}
2 changes: 1 addition & 1 deletion api/listings.js → api/_routes/listings.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { send, dbSelect, dbInsert, callFn, readBody, cache } from "./_lib.js";
import { send, dbSelect, dbInsert, callFn, readBody, cache } from "../_lib.js";

export default async function handler(req, res) {
const url = new URL(req.url, "http://x");
Expand Down
2 changes: 1 addition & 1 deletion api/report.js → api/_routes/report.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SUPA_FN, ANON } from "./_lib.js";
import { SUPA_FN, ANON } from "../_lib.js";
export default async function handler(req, res) {
const url = new URL(req.url, "http://x");
const mint = url.searchParams.get("mint") || "";
Expand Down
6 changes: 3 additions & 3 deletions api/screener.js → api/_routes/screener.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { jup, callFn, send, cache, dbSelect } from "./_lib.js";
import { normToken, num } from "./_normalize.js";
import { CELEB_MINTS, fetchMints } from "./_curated.js";
import { jup, callFn, send, cache, dbSelect } from "../_lib.js";
import { normToken, num } from "../_normalize.js";
import { CELEB_MINTS, fetchMints } from "../_curated.js";

const CHAINS = ["solana","ethereum","bsc","base","polygon","arbitrum","avalanche","sui","ton"];
const GT_HDR = { Accept: "application/json;version=20230302" };
Expand Down
4 changes: 2 additions & 2 deletions api/search.js → api/_routes/search.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { callFn, send } from "./_lib.js";
import { normToken, num } from "./_normalize.js";
import { callFn, send } from "../_lib.js";
import { normToken, num } from "../_normalize.js";

export default async function handler(req, res) {
const url = new URL(req.url, "http://x");
Expand Down
Loading