# Launch API

A program can launch a token the way the launch page does: quote it, get the transaction that pays for it, hand the paid launch over, and follow it until all five chains are live. The endpoints are under `https://www.pangea.trade/api/v1/launch`. Prepare, submit, status and the SOL payment are the launch page’s own steps, handed to the same code, with the same checks and the same limits.

- **No API key and no account.** Limits are per caller, by IP address, counted on each server. Keys for integrators, with limits of their own, may come later; they are not built.
- **Programs, and the Pangea site.** A program sends no `Origin` header and is served. A browser on another website always sends one, and is refused with `403`, so another site cannot spend the reads a quote makes on five chains through its visitors. The status read is a read and takes any origin. The event stream holds a connection open, so it takes the same rule as the rest and needs `Accept: text/event-stream` (406 without it); a page's no-cors or cross-site request is refused with 403, and each caller may hold one stream per launch.
- **The platform signs the snapshot, never the API.** Every chain launches with one price snapshot, signed by Pangea’s worker after it has found your payment on chain. No endpoint here signs one or sends anything for you.
- Amounts are base units as decimal strings: wei on an EVM chain, lamports on Solana, and nine decimals for the token.

## The flow

- `POST /v1/launch/prepare` quotes the launch and returns a `ticket`: the launch’s every parameter and its fee, signed by Pangea. Every later step takes it.
- `POST /v1/launch/authorization` only when another wallet sends your own chain’s launch: the permission your fee wallet signs for it. [Launch with your own wallet →](https://www.pangea.trade/app/docs#self-launch)
- `POST /v1/launch/pay` returns the one transaction that pays the fee. You send it from your wallet; Pangea sends nothing for you. Pay within the quote’s 10 minutes.
- `POST /v1/launch/submit` hands the launch over once the payment is mined, within 24 hours of the quote. Pangea checks the payment on chain and queues the launch.
- `GET /v1/launch/status` and `GET /v1/launch/events` follow it: the price snapshot signed, then each chain as it goes live, Solana first.

| Endpoint | What it is for | Limit per caller |
| --- | --- | --- |
| `POST /v1/launch/prepare` | The quote, and the ticket. | 12 in ten minutes, shared with the launch page. |
| `POST /v1/launch/authorization` | The permission your fee wallet signs for another launching wallet. | 30 in ten minutes |
| `POST /v1/launch/pay` | The transaction that pays the fee. | 30 in ten minutes; a SOL payment counts against the launch page’s own 30 as well. |
| `POST /v1/launch/submit` | Hands the paid launch to Pangea’s launcher. | 30 in ten minutes, shared with the launch page. |
| `GET /v1/launch/status` | Where the launch stands. | 300 in ten minutes, shared with the launch page. |
| `GET /v1/launch/events` | The same, as server-sent events. | 30 opened in ten minutes, 3 open at once. |
| `POST /v1/launch/kit` | What you need to make your own launch-time buys. | 30 in ten minutes |
| `POST /v1/launch/solana-cosigned` | Not available yet. | None. |

The whole flow, with fetch:

```js
const API = "https://www.pangea.trade/api/v1/launch";

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 get = (path) => fetch(API + path).then(async (r) => ({ status: r.status, body: await r.json() }));

// 1. The quote, and the ticket: pay, submit and the launch kit all take it.
const prepared = await post("/prepare", {
  name: "Example",
  symbol: "EXMPL",
  metadataURI: "ipfs://<the CID of your metadata document>",
  feeBps: 100,
  creatorEvm: "0x<your EVM fee wallet>",
  creatorSol: "<your Solana fee wallet>",
  payChain: "base",
});
if (prepared.status !== 200) throw new Error(prepared.body.error);
const { ticket, launchId } = prepared.body;
console.log(`fee: ${prepared.body.fee.payment.send} wei, about $${prepared.body.fee.feeUsd}`);

// 2. The transaction that pays it: send it from your wallet and wait for it to be mined.
const pay = await post("/pay", { ticket });
if (pay.status !== 200) throw new Error(pay.body.error);
const { chainId, to, data, value } = pay.body.tx;
console.log({ chainId, to, data, value, sendFrom: pay.body.from, before: pay.body.validUntil });
const payer = "0x<the wallet that sent it>";

// 3. Once it is mined, hand the launch over. A 402 means the payment is not found yet: ask again.
const submitted = await post("/submit", { ticket, payer });
if (submitted.status !== 200) throw new Error(submitted.body.error);

// 4. Where it stands: pending, running, done or failed, and each chain as it goes live.
const status = await get(`/status?launchId=${launchId}`);
console.log(status.body.state, Object.keys(status.body.result?.live ?? {}));
```

## Errors

An error is `{ "error": "..." }` with a status, and the message says what to do. A status read or an event stream for an id the queue does not have is `404` with `{ "launchId": "0x...", "state": "unknown" }` instead.

| Status | What it means |
| --- | --- |
| `400` | The request is wrong: a field missing or malformed, a ticket that does not verify or is over a day old. |
| `402` | Submit only: the payment is not in order. Not mined yet, or short: the answer carries `paid`, `quoted`, `collector` and `chain`. Ask again once it is. |
| `403` | A browser on another website. |
| `404` | A launch id the queue does not have. |
| `409` | The request conflicts with where the launch is: the quote has run out, a fee wallet to confirm (with `needsConfirm`), a pair or option that cannot be had, a kit for a launch not submitted yet, a permission for a launch that needs none, a payment for a launch already paid and queued (do not pay twice: a second payment toward the same launch id is kept as fee), and the co-signed Solana launch, which is not built yet. |
| `410` | A SOL payment asked for with too little of the quote’s life left for it to land. Prepare again. |
| `424` | Something Pangea depends on could not be read or is not configured: a chain, the launch queue, the launcher’s configuration. Launches closed is a 424 too. Nothing was queued; try again. |
| `429` | Over a limit. The message says how many seconds to wait; prepare and the event stream also send `Retry-After`. |

A body that is too large is `413`, one sent compressed `415`, and one that does not arrive in time `408`.

## POST /v1/launch/prepare

- Prepare, POST: `https://www.pangea.trade/api/v1/launch/prepare`

The body you POST:

```json
{
  "name": "Example",
  "symbol": "EXMPL",
  "metadataURI": "ipfs://<the CID of your metadata document>",
  "feeBps": 100,
  "creatorEvm": "0x<your EVM fee wallet>",
  "creatorSol": "<your Solana fee wallet>",
  "payChain": "base"
}
```

| Field | What it is |
| --- | --- |
| `name`, `symbol` | The token’s name, 1 to 32 bytes of UTF-8, and its ticker, 1 to 10 bytes with no space. Neither has to be unique. |
| `metadataURI` | `ipfs://` and the content hash of your token’s metadata document, nothing else. It is part of every EVM token’s address. The launch page pins the document for you, but its upload endpoint answers the Pangea site only: a program pins its own. |
| `feeBps` | The trading fee, 30 to 500 basis points. Set once, for good. |
| `creatorEvm`, `creatorSol` | Your fee wallets: where your share of the trading fee is paid, on the EVM chains and on Solana. A contract, or a Solana account that is not a plain wallet, is refused with `409` and `needsConfirm` unless `confirmFeeWallet` says it is yours. |
| `payChain` | `ethereum`, `base`, `robinhood`, `bsc` or `solana`; `base` when left out. You pay once, on this chain, in its native asset. |
| `devBuy` | Optional: your own first buy, in the pay chain’s smallest unit, as a string. Paid with the fee, from `creatorEvm` or `creatorSol`. |
| `pairs`, `pairSol` | Optional: what a chain’s pool pairs against when not its native coin, an asset that chain’s pair registry lists. |
| `openRoutes` | `true` to pay, in the same payment, for Solana’s four bridge routes to be opened right after the launch. |
| `selfLaunch` | `true` to send your pay chain’s launch yourself. EVM pay chains only. [Launch with your own wallet →](https://www.pangea.trade/app/docs#self-launch) |
| `selfLaunchFrom` | With `selfLaunch`: the wallet that sends it, when it is not `creatorEvm`. It then needs your fee wallet’s permission. |
| `confirmFeeWallet` | `{ evm: true }` or `{ sol: true }`: yes, that fee wallet is mine. |

The answer carries `launchId`, `ticket`, `creator`, `fee` (`feeUsd`, `costUsd`, `ttlSeconds` and `payment`: `chain`, `asset`, `wei`, `devBuy`, `send`, what the wallet sends in all, `collector` and, on an EVM chain, `tx`), `prediction.perChain`, each EVM chain’s token address, every one ending in `6ea`, `selfLaunch` when asked for, and `remaining`, the steps left in plain sentences.

## POST /v1/launch/authorization

- Authorization, POST: `https://www.pangea.trade/api/v1/launch/authorization`

Body: `{ ticket }`, for a launch quoted with `selfLaunch` and a `selfLaunchFrom` other than the fee wallet. The answer is `{ launchId, chain, signer, launcher, typedData, digest, next }`: `typedData` is what `signer`, your fee wallet, signs with `eth_signTypedData_v4`, and `digest` the hash the factory recovers it from, for a signer that signs hashes. It is the same typed data prepare returned as `selfLaunch.authorization`. Post the signature to submit as `creatorAuth`. `409` for a launch that needs no permission.

## POST /v1/launch/pay

- Pay, POST: `https://www.pangea.trade/api/v1/launch/pay`

Body: `{ ticket, payer }`. Every amount comes from the ticket, never from the request. On an EVM chain, `payer` is not needed and the answer is `{ launchId, chain, tx, fee, devBuy, from, validUntil, next }`: `tx` is `{ chainId, to, data, value }`, a call of `pay(launchId)`, or `payWithDevBuy(launchId, devBuy)`, on that chain’s LaunchFeeCollector, with the fee and the dev buy as its value. Send it from any wallet; with a dev buy, from `from`, your `creatorEvm`, or the buy is never made. `validUntil` is the end of the quote, in unix seconds. A dev buy on a chain paired with an asset is refused with `409`: send the quote’s own `fee.payment.tx` instead.

Paid in SOL, `payer` is the paying wallet in base58, and the answer is an unsigned versioned transaction naming it as the only signer: `{ launchId, programId, lamports, feeLamports, devBuyLamports, payer, transaction, feeAccount, blockhash, lastValidBlockHeight, rentLamports, quoteAccountRentLamports }`, `transaction` in base64. The payment’s own account needs `rentLamports` on top. Sign and send it within about a minute: it is good for as long as its blockhash. With a dev buy, the payer must be `creatorSol`.

Pay the launch fee in SOL with @solana/web3.js, then submit:

```ts
import { Connection, Keypair, VersionedTransaction } from "@solana/web3.js";

const API = "https://www.pangea.trade/api/v1/launch";
const ticket = process.env.LAUNCH_TICKET; // from prepare, with payChain solana
const payer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(process.env.SOLANA_KEYPAIR)));
const connection = new Connection(process.env.SOLANA_RPC, "confirmed");
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() }));

// The unsigned payment, naming your wallet as its only signer.
const pay = await post("/pay", { ticket, payer: payer.publicKey.toBase58() });
if (pay.status !== 200) throw new Error(pay.body.error);
const tx = VersionedTransaction.deserialize(Buffer.from(pay.body.transaction, "base64"));
console.log(`paying ${pay.body.lamports} lamports, and ${pay.body.rentLamports} of rent`);

tx.sign([payer]);
const signature = await connection.sendRawTransaction(tx.serialize());
const confirmed = await connection.confirmTransaction(
  { signature, blockhash: pay.body.blockhash, lastValidBlockHeight: pay.body.lastValidBlockHeight },
  "confirmed",
);
if (confirmed.value.err) throw new Error("the payment failed");

// A fee paid in SOL is found by who paid it: name the payer.
const submitted = await post("/submit", { ticket, payer: payer.publicKey.toBase58() });
if (submitted.status !== 200) throw new Error(submitted.body.error);
console.log(submitted.body.state);
```

## POST /v1/launch/submit

- Submit, POST: `https://www.pangea.trade/api/v1/launch/submit`

Body: `{ ticket, payer, creatorAuth }`, once the payment is mined. `payer` is required for a fee paid in SOL, because that payment is found by who paid it; on an EVM chain it is optional and only recorded. `creatorAuth` is needed when another wallet sends your own chain’s launch. Submit never refuses a paid launch over it: without one that recovers to your fee wallet for that wallet, the launch is queued all the same, the answer carries `selfLaunch: "dropped"` and a `warning`, and Pangea sends that chain like the others. So check it first: `POST /v1/launch/authorization` with `{ ticket, creatorAuth }` answers `check: { ok }`. The fee is read on chain, never taken from the request. The answer is `{ launchId, state, queued: true, enqueuedAt, creator, paid, next, selfLaunch?, warning? }` with `state: "pending"`. Submitting again queues nothing and answers `{ launchId, state, queued: false, message, result }`.

## GET /v1/launch/status

- Status, GET: `https://www.pangea.trade/api/v1/launch/status?launchId=0x...`

The answer is `{ launchId, state, message, lastError, attempts, enqueuedAt, result, selfChain }`, each field only when it is set. `state` is `pending`, `running`, `done` or `failed`. In `result`: `signed.at` when the price snapshot is signed, `live`, each chain as it goes live, `{ token, at, tx }`, and `selfLaunch`, your own launch call, for a launch that has one. `selfChain` names the chain you send yourself. `signed.at` and each chain’s `at` are ISO 8601 times; the call’s `takeoverAt` is in unix seconds.

## GET /v1/launch/events

- Events, GET: `https://www.pangea.trade/api/v1/launch/events?launchId=0x...`

The status as it changes, as server-sent events: each an `event: <name>` line and a `data: <json>` line. Everything so far is sent the moment the stream opens, so a late listener misses nothing. A comment line, starting with a colon, comes every 15 seconds to keep the connection open. The stream closes after `end`, after 30 minutes, or when you disconnect; open it again to keep following. An id the queue does not have is `404`: open the stream after submit has queued the launch.

| Event | Data | When |
| --- | --- | --- |
| `state` | `{ state }` | First, and whenever the state changes. |
| `signed` | `{ at }` | The price snapshot is signed. |
| `pool-live` | `{ chain, token, at, tx }` | That chain’s launch has landed and its pool is open. `tx` only when known. |
| `self-launch` | `{ chain, call }` | Your own launch call is out, once Solana has launched. `call` is the same as the status’s `result.selfLaunch`. |
| `self-launched` | `{ chain, by }` | `by` is `creator` or `platform`: who sent your chain’s launch. |
| `end` | `{ state }` | `done` or `failed`. The stream closes. |

At most 3 open streams per caller, and a few hundred in all on each server; past that it is `429` with `Retry-After`, and polling the status read does the same job. A page on the Pangea site can open it with an `EventSource`; a program reads it with a streaming fetch.

Read the event stream with fetch:

```js
const API = "https://www.pangea.trade/api/v1/launch";

// One frame: its event name and its JSON. Comment lines, such as the keep-alive, are skipped.
function parseFrame(frame) {
  let event = "message";
  const data = [];
  for (const line of frame.split("\n")) {
    if (line.startsWith(":")) continue;
    if (line.startsWith("event: ")) event = line.slice(7);
    else if (line.startsWith("data: ")) data.push(line.slice(6));
  }
  return data.length ? { event, data: JSON.parse(data.join("\n")) } : null;
}

async function* launchEvents(launchId) {
  const res = await fetch(`${API}/events?launchId=${launchId}`, { headers: { Accept: "text/event-stream" } });
  if (!res.ok) throw new Error(`the stream answered ${res.status}`);
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) return;
    buffer += decoder.decode(value, { stream: true });
    let end;
    while ((end = buffer.indexOf("\n\n")) >= 0) {
      const frame = parseFrame(buffer.slice(0, end));
      buffer = buffer.slice(end + 2);
      if (frame) yield frame;
    }
  }
}

for await (const { event, data } of launchEvents("0x<your launch id>")) {
  if (event === "pool-live") console.log(`${data.chain} is open: token ${data.token}`);
  if (event === "self-launch") console.log(`your launch call for ${data.chain} is out`, data.call);
  if (event === "self-launched") console.log(`${data.chain} was sent by the ${data.by}`);
  if (event === "end") break;
}
```

## POST /v1/launch/kit

- Launch kit, POST: `https://www.pangea.trade/api/v1/launch/kit`

Body: `{ ticket, buys, deadline }`, once the launch is submitted; before, it is `409`. Each EVM chain’s token address and pool, ready calldata for your own buys, your launch call if you send one, and Solana’s pool once Solana has launched. The fields are under [Launch-time buys →](https://www.pangea.trade/app/docs#self-launch)

## POST /v1/launch/solana-cosigned

Not available. It always answers `409`, with `enabled: false` and the reason: a Solana launch transaction signed by Pangea’s launcher, that you could submit yourself through Jito with your own buys after it, needs a change to the splitter program that is still in progress.

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