diff --git a/app/src/app/api/pairs/precheck/route.ts b/app/src/app/api/pairs/precheck/route.ts
index 643b4bd..78cb460 100644
--- a/app/src/app/api/pairs/precheck/route.ts
+++ b/app/src/app/api/pairs/precheck/route.ts
@@ -3,7 +3,7 @@ import { isAddress } from "viem";
import { rateLimited } from "@/lib/launchpad/editServer";
import { clientIp, readJson } from "@/lib/profiles/http";
import { applicationsOpen, precheckToken } from "@/lib/launchpad/pairApplyServer";
-import { PAIR_CHAINS } from "@/lib/launchpad/pairs";
+import { PAIR_CHAINS, PAIR_CHAINS_TEXT } from "@/lib/launchpad/pairs";
export const dynamic = "force-dynamic";
@@ -16,7 +16,7 @@ export async function POST(req: Request) {
if (rateLimited(`pairs-precheck:ip:${clientIp(req)}`, 6)) return NextResponse.json({ error: "slow down" }, { status: 429 });
const b = await readJson(req);
const chain = PAIR_CHAINS.find((c) => c === b?.chain);
- if (!chain || typeof b?.address !== "string" || !isAddress(b.address)) return NextResponse.json({ error: "a Base token address" }, { status: 400 });
+ if (!chain || typeof b?.address !== "string" || !isAddress(b.address)) return NextResponse.json({ error: `a token address on ${PAIR_CHAINS_TEXT}` }, { status: 400 });
try {
return NextResponse.json(await precheckToken(chain, b.address), { headers: { "cache-control": "no-store" } });
} catch (e) {
diff --git a/app/src/app/llms.txt/route.ts b/app/src/app/llms.txt/route.ts
index bfb4de8..6076587 100644
--- a/app/src/app/llms.txt/route.ts
+++ b/app/src/app/llms.txt/route.ts
@@ -4,12 +4,13 @@ import { BRAND_DOMAIN, BRAND_GITHUB } from "@/lib/brand";
import { boostEnabled } from "@/lib/launchpad/boostServer";
import { BOOST_FLOOR_TWIG, BOOST_TOKEN, BOOST_WINDOW_H } from "@/lib/launchpad/boost";
import { applicationsOpen } from "@/lib/launchpad/pairApplyServer";
+import { PAIR_CHAINS, PAIR_CHAINS_TEXT } from "@/lib/launchpad/pairs";
/** How to apply for a community pair, only while applications are open: an agent must never burn for one that cannot exist yet. */
function pairApplySection(): string {
if (!applicationsOpen()) return "";
- return `Listing is applied for at /pairs/apply (a free check, then a fee in TWIG sent to 0x…dEaD naming the application: amount = whole TWIG × 10^18
-+ 3 × 10^11 + application id; it pays for a review, not a listing, and is not refunded). Rules: https://github.com/Twigpine/openlaunch/blob/main/docs/pairs.md
+ return `Listing is applied for at /pairs/apply (a free check, then a fee in TWIG on Base, whichever chain the token is on, sent to 0x…dEaD naming the
+application: amount = whole TWIG × 10^18 + 3 × 10^11 + application id; it pays for a review, not a listing, and is not refunded). Rules: https://github.com/Twigpine/openlaunch/blob/main/docs/pairs.md
`;
}
@@ -97,13 +98,13 @@ Recommended: buy a little right after launch (a Universal Router swap on the new
holder and no price move reads as dead on screeners and sits under "quiet launches" on the home page until an outside wallet trades it.
${boostSection()}
-## Community pairs (Base)
+## Community pairs (${PAIR_CHAINS_TEXT})
A launch may be paired with a listed community token: the site badges it and prices it in USD from one named pool (checked against
that pool's 30-minute average). The launch form does not offer them (ETH, TWIG and stocks only): such a launch is made against the
factory directly. The contracts accept any ERC-20 either way; an unlisted one shows as "Unlisted pair", unpriced. Each listing is
checked hourly and suspended when its checks fail.
- GET ${SITE_URL}/api/pairs (every pair: address, symbol, decimals, status, usd, price source)
-Pages: /pairs, /pair/base/
. Launch with one against the factory exactly as with any ERC-20 quote (findSalt with the pair's address).
+Pages: /pairs, /pair// (chain: ${PAIR_CHAINS.join(" or ")}). Launch with one against the factory exactly as with any ERC-20 quote (findSalt with the pair's address).
${pairApplySection()}
## API
- GET ${SITE_URL}/api/launch/list?chain=base|robinhood|arc&sort=live|new|mcap|volume|gainers|holders&window=1h|24h|all&limit=50
diff --git a/app/src/app/pairs/apply/[id]/page.tsx b/app/src/app/pairs/apply/[id]/page.tsx
index 4fc1f44..26b6af3 100644
--- a/app/src/app/pairs/apply/[id]/page.tsx
+++ b/app/src/app/pairs/apply/[id]/page.tsx
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
+import { CHAIN_LABELS } from "@/lib/chainPublic";
import { notFound } from "next/navigation";
import SectionIntro from "@/components/sections/SectionIntro";
import shell from "@/components/sections/SectionShell.module.css";
@@ -14,7 +15,7 @@ export default async function PairApplicationPage({ params }: { params: Promise<
if (!app) notFound();
return (
-
+
diff --git a/app/src/components/admin/AdminSignIn.tsx b/app/src/components/admin/AdminSignIn.tsx
index ab39fd8..8c3ab88 100644
--- a/app/src/components/admin/AdminSignIn.tsx
+++ b/app/src/components/admin/AdminSignIn.tsx
@@ -3,7 +3,7 @@
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { useConfig } from "wagmi";
-import { getWalletClient } from "wagmi/actions";
+import { walletOnChain } from "@/lib/walletOnChain";
import ConnectButton from "@/components/ConnectButton";
import { btn, card } from "@/components/ui";
import { useHydratedAccount } from "@/lib/useHydratedAccount";
@@ -45,7 +45,7 @@ export default function AdminSignIn() {
const nd = (await n.json().catch(() => ({}))) as { nonce?: string; issuedAt?: string; error?: string };
if (!n.ok || !nd.nonce || !nd.issuedAt) throw new Error(nd.error ?? `request failed (HTTP ${n.status})`);
const message = buildAdminSignInMessage({ origin: window.location.origin, address, nonce: nd.nonce, issuedAt: new Date(nd.issuedAt) });
- const wallet = await getWalletClient(config, { chainId: CHAINS.base.id });
+ const wallet = await walletOnChain(config, CHAINS.base.id);
const signature = await wallet.signMessage({ account: address, message });
const res = await fetch("/api/admin/session", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ message, signature }) });
const d = (await res.json().catch(() => ({}))) as { error?: string };
diff --git a/app/src/components/launchpad/PairApplication.tsx b/app/src/components/launchpad/PairApplication.tsx
index f3710c5..bc653b7 100644
--- a/app/src/components/launchpad/PairApplication.tsx
+++ b/app/src/components/launchpad/PairApplication.tsx
@@ -4,24 +4,26 @@ import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import { useConfig, useChainId, useReadContract, useSwitchChain } from "wagmi";
import { getPublicClient, getWalletClient } from "wagmi/actions";
+import { walletOnChain } from "@/lib/walletOnChain";
import type { Hex } from "viem";
import { useHydratedAccount } from "@/lib/useHydratedAccount";
import { btn, card, helper, input } from "@/components/ui";
-import { BUILDER_DATA_SUFFIX, CHAINS, explorerTx, shortAddr } from "@/lib/chainPublic";
+import { BUILDER_DATA_SUFFIX, CHAINS, CHAIN_LABELS, explorerAddress, explorerTx, shortAddr } from "@/lib/chainPublic";
import { ERC20_MIN_ABI, ERC20_TRANSFER_ABI } from "@/lib/launchpad/abi";
import { DEAD } from "@/lib/launchpad/config";
import { TWIG_ADDRESS, TWIG_WRAP_URL } from "@/lib/launchpad/twig";
import { nowMs } from "@/lib/launchpad/time";
import { nonce } from "@/lib/nonce";
import { friendlyError } from "@/lib/errors";
-import { pairPath } from "@/lib/launchpad/pairs";
+import { PAIR_FEE_CHAIN, pairPath } from "@/lib/launchpad/pairs";
import { PAIR_APPLY_COPY, buildPairApplyMessage, pairApplicationOf, proofPostText, proofPostUrl, type PairApplyAction } from "@/lib/launchpad/pairApply";
import { intentUrl } from "@/lib/profiles/xpost";
import type { PairApplicationPublic } from "@/lib/launchpad/pairApplyServer";
import PrecheckReport from "./PrecheckReport";
-const CHAIN = "base" as const;
-const BASE = CHAINS[CHAIN];
+/** The fee is TWIG on Base and every signature here is made on Base, whatever chain the token is on. */
+const FEE_CHAIN = PAIR_FEE_CHAIN;
+const BASE = CHAINS[FEE_CHAIN];
const group = (v: string | bigint) => BigInt(v).toLocaleString("en-US");
/**
@@ -104,9 +106,9 @@ export default function PairApplication({ initial, open }: { initial: PairApplic
try {
const n = nonce();
const ts = nowMs();
- const wallet = await getWalletClient(config, { chainId: BASE.id });
- const signature = await wallet.signMessage({ message: buildPairApplyMessage({ action, chain: CHAIN, address: app.address, wallet: address, nonce: n, ts, id: app.id, ...extra }) });
- const res = await fetch("/api/pairs/apply", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action, chain: CHAIN, address: app.address, id: app.id, wallet: address, nonce: n, ts, signature, ...extra }) });
+ const wallet = await walletOnChain(config, BASE.id);
+ const signature = await wallet.signMessage({ message: buildPairApplyMessage({ action, chain: app.chain, address: app.address, wallet: address, nonce: n, ts, id: app.id, ...extra }) });
+ const res = await fetch("/api/pairs/apply", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action, chain: app.chain, address: app.address, id: app.id, wallet: address, nonce: n, ts, signature, ...extra }) });
const d = (await res.json().catch(() => ({}))) as { error?: string };
if (!res.ok) throw new Error(d.error ?? `request failed (HTTP ${res.status})`);
setMsg({ ok: true, text: action === "proof" ? "Proof post saved." : "Sent back for review." });
@@ -123,7 +125,7 @@ export default function PairApplication({ initial, open }: { initial: PairApplic
if (!sent || busy) return;
setBusy("recheck");
try {
- await fetch(`/api/launch/sync?chain=${CHAIN}&tx=${sent}`, { method: "POST" }).catch(() => {});
+ await fetch(`/api/launch/sync?chain=${FEE_CHAIN}&tx=${sent}`, { method: "POST" }).catch(() => {});
await reload();
} finally {
setBusy(null);
@@ -175,7 +177,7 @@ export default function PairApplication({ initial, open }: { initial: PairApplic
setMsg({ ok: false, text: e instanceof Error && /reverted/.test(e.message) ? e.message : "Sent. Its confirmation is not in yet: this page updates when it is indexed." });
return;
}
- await fetch(`/api/launch/sync?chain=${CHAIN}&tx=${hash}`, { method: "POST" }).catch(() => {});
+ await fetch(`/api/launch/sync?chain=${FEE_CHAIN}&tx=${hash}`, { method: "POST" }).catch(() => {});
setMsg({ ok: true, text: "Sent. It counts once indexed (seconds)." });
await reload();
void bal.refetch();
@@ -202,7 +204,9 @@ export default function PairApplication({ initial, open }: { initial: PairApplic
{app.can_ask_again ? (
mine ? :
The applicant can ask for another review ({app.reviews_max - app.reviews_used} left).
) : null}
-
Applied by {shortAddr(app.applicant)} · {app.reviews_used} of {app.reviews_max} reviews used.