# Launch with your own wallet

Every launch is on five chains: Solana, Ethereum, Base, BSC and Robinhood Chain. Solana always launches first, from Pangea’s launcher: only launchers the splitter program lists can send it, and its addresses derive from a secret seed that stays unpublished until it lands. Then the four EVM chains launch.

With `selfLaunch: true`, you send one EVM chain’s launch yourself, the chain you pay on. Pangea’s launchers send the other four, as for every launch.

## Why nobody can launch your chain before you

Each EVM factory has two ways in. `launch(LaunchParams p, Snapshot snap, bytes signature)` takes three senders only: your fee wallet (`msg.sender == p.creator`), the factory’s admin, and a launcher the admin has listed. Anyone else reverts with `NotLauncher`.

`launchAuthorized(LaunchParams p, Snapshot snap, bytes signature, bytes creatorAuth)` takes any sender that holds your fee wallet’s permission naming that sender. The permission is EIP-712 typed data, `LaunchAuth(bytes32 launchId, address launcher)`, in the domain `{ name: "PangeaLaunchAuth", version: "1" }`, with no chain id and no contract, so one signature covers all four EVM chains. The factory recovers it against the sender, so a permission for one wallet is useless to anyone else who sees it; a signature that recovers to anyone but your fee wallet reverts with `BadCreatorAuth`.

So nobody else can send your chain’s launch, and it cannot be front-run. The price snapshot it carries exists only after your fee is paid: Pangea’s worker signs it after checking your payment on chain, and the API never signs one.

> **Note:** A fee wallet that is a contract cannot sign the permission: there is no EIP-1271 check. Launch from the fee wallet itself, or let Pangea send that chain. Launching through a tokenomics contract comes later, with tokenomics.

## The fee

The same as for any launch, quoted by prepare and paid once. With `selfLaunch`, the pay chain must be an EVM chain, and it is the chain you launch. Your own launch transaction releases the fee to the platform in the same transaction (`LaunchFeeCollector.onLaunch`). Sending your chain yourself does not change the fee.

## Your time to send it

- Your call is handed out once Solana’s launch has landed. Pangea sends the other three EVM chains at the same time.
- The option is on only where the deployment turns it on; elsewhere prepare refuses selfLaunch. Your time is counted from when Solana’s launch lands: 90 seconds unless the deployment sets `SELF_LAUNCH_GRACE_S` to another, held between 90 and 120. It always ends at least 300 seconds before the snapshot’s deadline, and if the launch is retried it never ends later than the first time it was given. Its end is `takeoverAt` in your call, in unix seconds.
- If your launch is not there by then, Pangea’s launcher sends it, with the same snapshot and the same parameters. Whichever of the two lands second reverts with `AlreadyLaunched`, and its sender loses only that transaction’s gas.
- If another EVM chain fails, Pangea still launches yours once your time is up.
- The launch page stops offering the send 30 seconds before the takeover, since a transaction sent later may land after Pangea’s.

## Step by step

- `POST /v1/launch/prepare` with `selfLaunch: true`, `payChain` set to your chain, `creatorEvm` set to your fee wallet and, if another wallet will send the launch, `selfLaunchFrom` set to it. The answer’s `selfLaunch` is `{ chain, chainId, from, creator, factory, token, gasLimit, value, graceSeconds, authorization }`.
- If `authorization` is set, sign it with the fee wallet (`eth_signTypedData_v4`). You can also get it from `POST /v1/launch/authorization` with `{ ticket }`, which answers `{ launchId, chain, signer, launcher, typedData, digest }`. Sign before you pay. The typed data names its own domain type (`EIP712Domain: [{ name: "name", type: "string" }, { name: "version", type: "string" }]` in its `types`), so pass it whole: through `eth_signTypedData_v4` or viem’s `signTypedData`, a wallet hashes the domain the contract checks.
- `POST /v1/launch/pay` with `{ ticket }` answers `{ tx: { chainId, to, data, value }, from, validUntil }`. Send `tx` on your chain; with a dev buy, from `from`.
- `POST /v1/launch/submit` with `{ ticket, payer, creatorAuth }` once it is mined. `creatorAuth` is the signature, required when the launching wallet is not the fee wallet.
- Watch `GET /v1/launch/events?launchId=` or poll `GET /v1/launch/status?launchId=`. When Solana is live, the status’s `result.selfLaunch` and the `self-launch` event carry your call: `{ chain, chainId, launchId, from, to, data, value, gas, token, deadline, takeoverAt }`, where `to` is the factory and `data` the encoded `launch`, or `launchAuthorized` with your permission, carrying the signed snapshot.
- Send exactly that transaction, from `from`, before `takeoverAt`. Then `result.selfLaunch.by` says `creator` or `platform`; the status’s top-level `selfChain` names your chain throughout.

Sign the fee wallet’s permission for another wallet, with viem:

```ts
import { privateKeyToAccount } from "viem/accounts";

const API = "https://www.pangea.trade/api/v1/launch";
const ticket = process.env.LAUNCH_TICKET; // from prepare, with selfLaunch and selfLaunchFrom
// The FEE WALLET signs: the creatorEvm you named. It lets the launching wallet send this launch.
const feeWallet = privateKeyToAccount(process.env.FEE_WALLET_KEY);
const post = (path, body) =>
  fetch(API + path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
    .then(async (r) => ({ status: r.status, body: await r.json() }));

const auth = await post("/authorization", { ticket });
if (auth.status !== 200) throw new Error(auth.body.error);
const { signer, launcher, typedData } = auth.body;
if (signer.toLowerCase() !== feeWallet.address.toLowerCase()) throw new Error(`sign with ${signer}`);

// One signature for all four EVM chains: the domain has no chain id and no contract.
const creatorAuth = await feeWallet.signTypedData(typedData);

// Through eth_signTypedData_v4 yourself, pass it whole: it already names its domain's type (EIP712Domain).
const forWallet = typedData;
console.log("eth_signTypedData_v4 params:", [signer, JSON.stringify(forWallet)]);
console.log(`${launcher} may now launch ${auth.body.launchId}, and nobody else may use this`);

// Pay as usual, then hand it over with the permission.
const submitted = await post("/submit", { ticket, payer: "0x<the wallet that paid>", creatorAuth });
if (submitted.status !== 200) throw new Error(submitted.body.error);
```

Send your own chain’s launch, with viem:

```ts
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base, bsc, mainnet, robinhood } from "viem/chains";

const API = "https://www.pangea.trade/api/v1/launch";
const CHAINS = { 1: mainnet, 8453: base, 56: bsc, 4663: robinhood };
const launchId = "0x<your launch id>";

// The wallet that launches: your fee wallet, or the selfLaunchFrom you named.
const account = privateKeyToAccount(process.env.LAUNCHER_KEY);

const status = await fetch(`${API}/status?launchId=${launchId}`).then((r) => r.json());
const call = status.result?.selfLaunch;
if (!call?.data) throw new Error("not out yet: it is handed out once Solana has launched");
if (call.by) throw new Error(`already sent, by the ${call.by}`);
if (Date.now() / 1000 >= call.takeoverAt) throw new Error("too late: Pangea is sending it, with the same snapshot");
if (call.from.toLowerCase() !== account.address.toLowerCase()) throw new Error(`send it from ${call.from}`);

const chain = CHAINS[call.chainId];
const transport = http(process.env.RPC_URL);
const { maxFeePerGas, maxPriorityFeePerGas } = await createPublicClient({ chain, transport }).estimateFeesPerGas();
// What the wallet must hold: the whole gas limit at the max fee, and the value. Unused gas is refunded.
const mustHold = BigInt(call.gas) * maxFeePerGas + BigInt(call.value);
console.log(`hold at least ${mustHold} wei on ${chain.name}`);

const wallet = createWalletClient({ account, chain, transport });
const hash = await wallet.sendTransaction({
  to: call.to, // the factory
  data: call.data, // launch, or launchAuthorized with the permission, and the signed snapshot
  value: BigInt(call.value), // 1,000,000 wei: the opening buy
  gas: BigInt(call.gas),
  maxFeePerGas,
  maxPriorityFeePerGas,
});
console.log(`sent ${hash}; the event stream says self-launched when it lands`);
```

The examples sign with a private key to stay short. Any wallet that can sign typed data, and send a transaction with this data, value and gas limit, will do.

| Chain | Chain id | Factory | Fee collector | Universal Router |
| --- | --- | --- | --- | --- |
| Ethereum | 1 | `0x1D583e6eA4AA7c5e167a7E69B07b83DD5B813f53` | `0x79d9a01B6ADB4938761e7D4fb29a802ea709F0Ec` | `0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85` |
| Base | 8453 | `0xba4beE425F268f7Dc0F7e5457d14320d5A64cd06` | `0x7cb08218648433bbC9671f885E15954756717BCa` | `0xd6145b2D3F379919E8CdEda7B97e37c4b2Ca9c40` |
| BSC | 56 | `0xd1454764E8D87B117a23491E77cAfcCC50430256` | `0xcDB037B5B4b58e3C89d632463656D889123a7c04` | `0xDc264714F68d84CF29BC605589405E78bDBE7C9f` |
| Robinhood Chain | 4663 | `0x8A1F3e1DB07048c007F6d9ab924494C77c0DBa65` | `0xcDB037B5B4b58e3C89d632463656D889123a7c04` | `0x204FAca1764B154221e35c0d20aBb3c525710498` |

- Solana splitter program: `Hb5uTppRbNxc3RjVRqTWqfdU3E3GiZyGYKccxYf9tUej`

## Gas

Your call comes with a gas limit of `10,424,700`: a launch’s measured 7,290,000 gas, with 10% and then 30% of room. About 7.29 million gas is used and the rest is refunded, but to send it your wallet has to hold the whole gas limit at its max fee per gas, plus the value.

The value is `1,000,000` wei: the opening buy, the pool’s first trade, made in the launch only when the launch has no dev buy. Send it as given: zero would skip the opening buy (and the token page could not tell the launch transaction's buys apart), and any other value reverts.

- **What your wallet must hold.** At least `10,424,700 × max fee per gas + 1,000,000 wei`, in ETH on Ethereum, Base and Robinhood Chain, in BNB on BSC. On Base, add its L1 data fee, which is charged on top of gas × price: a few cents is ample. The figure above is a floor: the call's `gas` is the node's estimate from your wallet with a 30% margin, never less than it, so fund from `call.gas` once the call is out.
- **What it costs you.** About `7,290,000 × the gas price paid`, plus the value. At 1 gwei that is 0.00729 of the chain’s coin. Read the chain’s gas price before you send.

## Launch-time buys

Pangea sends nobody’s buys. The launch kit gives you what you need to make your own, from your own wallets, and leaves the sending to you.

`POST /v1/launch/kit` with `{ ticket, buys, deadline }` answers once the launch is paid and submitted; before, it is `409`. `buys` is optional: at most 20 of `{ chain, amountIn, minOut }`, EVM chains only, `amountIn` in the native coin’s wei and `minOut` in the token’s base units, both positive whole numbers as strings. `deadline` is optional too: unix seconds between 60 seconds and 1 hour from now, 1 hour when left out.

| Field | What it is |
| --- | --- |
| `launchId`, `state` | The launch, and where it stands in the queue. |
| `evm[]` | Each EVM chain: `chain`, `chainId`, `token`, `quote`, `factory`, `poolKey`, `poolId`, `router`, the Universal Router, and `live`, whether its pool is open. |
| `poolKey` | `{ currency0, currency1, fee: 0, tickSpacing: 60, hooks }`, with the native coin as the zero address and `hooks` the factory’s fee hook. `poolId` is `keccak256(abi.encode(poolKey))`. |
| `buys[]` | `{ chain, chainId, to, data, value, minOut, deadline }`: a call of the Universal Router’s `execute` with one `V4_SWAP`, an exact-in single swap then `SETTLE_ALL` and `TAKE_ALL`. The tokens go to the wallet that sends it. `value` is your amountIn. |
| `validUntil` | The deadline every buy above carries, in unix seconds. Ask again for later ones. |
| `selfLaunch` | `{ chain, call }` for a launch you send yourself: `call` is your launch call once it is out, and null until then. |
| `solana` | `{ available: false, reason }` until Solana has launched, because its addresses are secret until then. Then `{ available: true, venue, programId, pool, mint, tokenDecimals, swap }`: the pool is on Meteora DAMM v2, program `cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG`, and `swap` says how to swap on it. |
| `solanaCosignedLaunch` | `{ enabled: false }` with the reason. Not available yet. |

Buy calldata is built for pools paired with the native coin only; a pool paired with an asset is refused with `409`. `minOut` is required: the pool does not exist before the launch, so there is no quote to take it from, and a buy that would get less reverts. Work it out from the opening price the quote gives, less the trading fee, your own buy’s price impact and the slippage you accept.

Ask for the launch kit and send a buy once its pool is open, with viem:

```ts
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base, bsc, mainnet, robinhood } from "viem/chains";

const API = "https://www.pangea.trade/api/v1/launch";
const CHAINS = { 1: mainnet, 8453: base, 56: bsc, 4663: robinhood };

// The ticket prepare gave you, and a wallet to buy from: any wallet, and it receives the tokens.
const ticket = process.env.LAUNCH_TICKET;
const buyer = privateKeyToAccount(process.env.BUYER_KEY);

const res = await fetch(`${API}/kit`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  // 0.01 ETH on Base. At the opening price ($3,000 for a chain's 1B tokens) and ETH at $3,000 that is
  // about 10,000,000 tokens before the pool's fee and the price moving; ask for at least 9,000,000 (9 decimals).
  // Work minOut out from your own prices: it is the floor below which the buy reverts.
  body: JSON.stringify({ ticket, buys: [{ chain: "base", amountIn: "10000000000000000", minOut: "9000000000000000" }] }),
});
const kit = await res.json();
if (!res.ok) throw new Error(kit.error);

const [buy] = kit.buys;
const pool = kit.evm.find((c) => c.chain === buy.chain);
console.log(`pool ${pool.poolId}, token ${pool.token}, buy valid until ${kit.validUntil}`);
// A buy works only once its pool is open: wait for pool-live on the event stream, or ask again.
if (!pool.live) throw new Error(`the ${buy.chain} pool is not open yet`);

const wallet = createWalletClient({ account: buyer, chain: CHAINS[buy.chainId], transport: http(process.env.RPC_URL) });
const hash = await wallet.sendTransaction({ to: buy.to, data: buy.data, value: BigInt(buy.value) });
console.log("sent", hash);
```

## Making your buys land with your launch

- **Ethereum.** Sign your launch transaction and each buy, each buy from its own wallet with its own nonce and an explicit gas limit, since an estimate fails while the pool does not exist. Send them to a private relay such as Flashbots in one submission for one target block, your launch first. Nobody sees them before that block.
- **BSC.** The same, through a private submission to 48 Club or bloXroute: all of them for one block, your launch first.
- **Base and Robinhood Chain.** There is no public private relay. Send your launch, watch the event stream for `pool-live` on that chain, then send your buys. Others can buy in the same block.
- **Solana.** Pangea’s launcher sends Solana’s launch. Once it has landed, the kit gives its pool and mint, and you swap on Meteora like anyone. A launcher-signed Solana launch that you could submit through Jito with your buys after it is not available: `POST /v1/launch/solana-cosigned` answers `409`.

> **Note:** Every buy that lands in a chain’s launch block, yours or anyone’s, from any wallet, is shown publicly on the token’s page under “Bought in the launch block”: the launch block only, as the trade recorder read it, so buys in later blocks -- on Base and Robinhood Chain, where you buy after the pool opens, usually the next block or two -- are not listed there. Buys inside the launch transaction itself are shown as “In the launch tx”. Solana is not covered: its launch has no recorded slot.

## Risks

- Without a private relay, a buy can be front-run in the same block. `minOut` is what protects you: below it, the buy reverts.
- A signed snapshot expires: its deadline is never more than 1 hour away, and normally 15 minutes. After it your launch call stops working, whoever sends it.
- If your launch transaction lands after Pangea’s takeover, it reverts and you lose only its gas. The launch is made either way, with the same price and settings.
- A buy sent before its pool is open reverts, and costs its gas.

Part of the Pangea documentation: https://www.pangea.trade/app/docs.md
