Skip to Content
APILaunch API

Launch API

Create and manage token launches.

Create Launch

POST /launch

Creates a new token launch with optional vesting and DAO configuration.

Request Body

interface CreateLaunchRequest { // Required name: string; // Token name (max 32 chars) ticker: string; // Token symbol (max 10 chars) totalSupply: number; // Total supply (before decimals) // Optional description?: string; // Token description image?: string; // Image URL (Arweave/IPFS recommended) decimals?: number; // Token decimals (default: 9) reservePercent?: number; // % of supply for reserve (default: 0) beneficiary?: string; // Wallet to receive vested tokens // Vesting config vestingConfig?: { mode: 'time' | 'hybrid'; cliffMonths?: number; durationMonths?: number; time?: { // For hybrid mode percent: number; cliffMonths: number; durationMonths: number; }; }; // DAO config daoConfig?: { name?: string; // DAO name (default: token name) minTokensToCreate?: number; // Min tokens to create proposal votingTime?: number; // Voting period in seconds }; // Liquidity config liquidityConfig?: { percent: number; // % of reserve for liquidity initialPrice?: number; // Initial token price in SOL }; // Webhook webhookUrl?: string; // URL for status updates webhookSecret?: string; // HMAC secret for verification // Upstream linkage + idempotency externalProjectId?: string; // Upstream project id (e.g. a Studio project), max 128 chars product?: string; // Upstream product that requested the launch (e.g. "studio"), max 32 chars idempotencyKey?: string; // Client-generated key (max 64 chars). Re-sending the same // wallet + key returns the ORIGINAL launch's eventId (202) — // no new launch, no daily-limit bump. Enforced by a unique // index on (wallet, idempotencyKey), so concurrent duplicates // also collapse onto the first launch. }

Non-custodial guarantees

Every launch is executed so that Etch holds nothing of value once the job finishes:

  • The mint is created with no freeze authority (set to null in the same transaction).
  • The mint authority is revoked right after on-chain metadata is written (mintAuthorityRevoked: true), so supply is fixed forever.
  • All Meteora LP tokens are locked permanently (lpLocked: true) in two Meteora lock escrows: 90 % owned by the beneficiary (the creator claims those trading fees) and 10 % owned by the Etch wallet (platformFeeBps: 1000, the platform fee share). Nobody — not the creator, not Etch — can ever withdraw the liquidity; each escrow owner only claims its share of the pool’s trading fees (see Fees API). No LP tokens are ever transferred unlocked. Etch pays for and signs both lock transactions; the creator never has to sign. lpLock.creatorEscrow / lpLock.platformEscrow are the two escrow accounts.
  • Reserve tokens go to the beneficiary, a Streamflow vesting contract, or the DAO treasury depending on the configuration.

Launches made before the lock model (lpTransferSignature set, lpLocked: false) handed the LP tokens to the beneficiary unlocked and are left as-is.

The job is resumable and single-flight: every step’s result is persisted immediately, so a retry (QStash) or an admin replay resumes from the last completed step instead of minting a second token. The mint address is derived deterministically per launch and persisted before the create transaction is sent, so a retry checks the chain and skips the send when the mint already exists. Before doing any on-chain work an invocation claims a row lock; a second invocation that overlaps a live run (QStash retry racing a slow run, admin replay mid-run) returns 409 { "status": "in_progress" } with Retry-After: 60 and does nothing — QStash honours the header and re-delivers later. Locks are released when the run ends and expire after 10 minutes; an hourly sweeper re-enqueues any launch whose invocation was killed mid-run (non-terminal status with a lock older than 10 minutes) and parks a dead-letter-queue row for it.

Example Request

curl -X POST https://etch.film.fun/api/tokens/launch \ -H "Authorization: Bearer $ETCH_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Awesome Token", "ticker": "AWE", "description": "The most awesome token on Solana", "imageUrl": "https://example.com/awe.png", "totalSupply": 1000000000, "beneficiary": "8vP2...", "allocation": { "launchPercent": 60, "vestingPercent": 20, "daoPercent": 20 }, "vesting": { "mode": "time", "cliffMonths": 6, "durationMonths": 24 }, "dao": { "enabled": true, "name": "Awesome DAO", "passThreshold": 60, "votingPeriodHours": 72 }, "idempotencyKey": "6f0c1f9e-6f1f-4b7e-9d8b-1d1c2a7f3b10" }'

$ETCH_JWT is the token from sign-in. Register webhooks separately with POST /api/webhooks.

Response

202 Accepted

{ "eventId": "0b9f6c2e-3c1d-4d8e-9a51-7f0f2b6d8e11" }

Poll GET /api/tokens/launch/status/:eventId (or listen for launch.completed). Errors:

StatuserrorMeaning
400invalid_request / invalid_dbc / infeasible_curveValidation failed (see details)
401unauthorizedMissing or invalid JWT
403dbc_disabledmode: "dbc" while bonding-curve launches are off
409partner_fee_claimer_missingMainnet bonding-curve launch without DBC_PARTNER_FEE_CLAIMER
429rate_limitedThe wallet’s daily launch allowance is used up (resets 00:00 UTC)
429platform_daily_limitEtch’s platform-wide daily cap (ETCH_MAX_LAUNCHES_PER_DAY) is reached
503queue_unavailableThe job could not be queued; retry with the same idempotencyKey

The daily allowance is reserved atomically when the launch is accepted and handed back if the job could not be queued.

Get Launch Status

GET /launch/:launchId

Retrieve the status and details of a launch.

Response

{ "launchId": "launch_abc123xyz", "status": "complete", "createdAt": "2026-02-15T12:00:00Z", "completedAt": "2026-02-15T12:00:30Z", "config": { "name": "Awesome Token", "ticker": "AWE", "totalSupply": 1000000000, "reservePercent": 20 }, "stepErrors": {}, "externalProjectId": "studio-project-uuid", "product": "studio", "mintAuthorityRevoked": true, "lpLocked": true, "platformFeeBps": 1000, "lpLock": { "creatorEscrow": "ESCROWc...", "platformEscrow": "ESCROWp...", "creatorSignature": "4xyz...", "platformSignature": "5abc..." }, "lpTransferSignature": null, "result": { "mint": "AWE1234...", "metadata": "META5678...", "vestingContract": "VEST9012...", "dao": { "realm": "REALM3456...", "governance": "GOV7890...", "treasury": "TRES1234..." }, "pool": { "address": "POOL5678...", "lpMint": "LP9012..." } }, "transactions": [ { "type": "mint", "signature": "5abc...", "status": "confirmed" }, { "type": "vesting", "signature": "5def...", "status": "confirmed" } ] }

Status Values

StatusDescription
queuedLaunch is queued for processing
creating_tokenMint is being created
creating_poolMint exists; metadata, authority revocation and pool creation in progress
creating_vestingStreamflow vesting contract being created
creating_daoRealms DAO being created / funded
successAll steps completed successfully
partialMint, metadata and pool succeeded, but one or more non-fatal steps failed — see stepErrors. The launch is also parked in the dead-letter queue so an admin can replay it
failedA fatal step (mint, metadata, mint-authority revoke, pool) failed (see error); the job is retried and parked in the dead-letter queue

Metadata is fatal on purpose: it must be written while Etch still holds the mint authority, so a launch never ends up with a permanently metadata-less token. Because the mint address is persisted before anything is sent, a retry after a metadata failure resumes at the metadata step with the same mint. The one exception is a mint whose authority is already revoked (an older launch that predates the metadata step): metadata can never be created for it, so the job records stepErrors.metadata and continues instead of failing forever.

Step errors

stepErrors is an object keyed by step name with the failure message. Only non-fatal steps appear here:

KeyStep
metadataOnly when the mint authority was already revoked before metadata could be written (otherwise metadata is fatal, see above)
lpPermanent LP lock (creator + platform escrows)
reserveLegacy reserve transfer to the beneficiary
vestingStreamflow vesting contract
daoRealms DAO creation
daoTreasuryDAO treasury funding
{ "status": "partial", "stepErrors": { "vesting": "Streamflow: insufficient SOL for rent" }, "mint": "AWE1234...", "pool": "POOL5678...", "mintAuthorityRevoked": true }

A partial launch is tradeable (mint + pool exist). Every partial run upserts a pending dead-letter-queue row for the launch (error: "partial: {…stepErrors}"). The hourly cleanup cron re-runs it automatically (15 min backoff, doubling, up to 5 runs counted on that DLQ row); a failed launch that already has a mint on chain is retried the same way, while bad input (validate:), never-queued (queue:) and abandoned launches are not. After that an admin can replay it with POST /api/admin/dlq/:id/replay; the replay re-runs only the steps listed in stepErrors (each is idempotent on-chain: an existing Streamflow stream or Realms realm is reused rather than duplicated) and, when it ends in success, resolves that DLQ row automatically.

LP lock fields

FieldDescription
lpLockedtrue once ALL LP tokens sit in the two lock escrows. The step is idempotent: a replay checks both escrows on-chain and only locks what is still unlocked
platformFeeBpsShare of LP (basis points of 10 000) locked for the platform — 1000 = 10 %. The remaining 90 % is locked for the beneficiary
lpLock.creatorEscrowMeteora lock escrow owned by the beneficiary. Its owner claims the creator share of trading fees
lpLock.platformEscrowMeteora lock escrow owned by the Etch wallet (platform share). When the beneficiary is the Etch wallet a single escrow holds 100 % and both fields carry the same address
lpLock.creatorSignature / lpLock.platformSignatureThe two lock transactions (null for a share that was already locked when the job resumed)
lpTransferSignatureLegacy launches only: the unlocked LP hand-off transaction. null on locked launches

Wallet pre-flight

Before claiming a launch the job reads the Etch wallet balance. Below ETCH_LAUNCH_MIN_SOL (default 0.5) the launch is marked failed with error: "wallet_low_balance", a dead-letter-queue row is upserted, and the job answers 503 with Retry-After: 300 without touching the chain — QStash re-delivers once the wallet has been topped up, and the retry runs the launch normally. GET /api/health reports the same balance under checks.wallet.

List Launches

GET /launches

List all launches for your account.

Query Parameters

ParameterTypeDescription
statusstringFilter by status
limitnumberMax results (default: 20)
offsetnumberPagination offset

Example

curl "https://etch.film.fun/api/launches?status=complete&limit=10" \ -H "Authorization: Bearer etch_sk_live_xxxxx"
Last updated on