TypeScript REST API for creating Doppler launches.
This project is in active development & not ready for production use.
POST /v1/launchesPOST /v1/solana/launchesGET /v1/solana/launches/:launchAddressPOST /v1/launches/multicurve(alias)POST /v1/launches/static(alias)POST /v1/launches/dynamic(alias)GET /v1/launches/:launchIdGET /v1/capabilitiesGET /metricsGET /healthGET /ready
Auth model:
x-api-keyis required on all endpoints exceptGET /health.
- Error envelope shape:
{ "error": { "code", "message", "details?" } } - Rate limiting returns
429with codeRATE_LIMITED. GET /healthrate limits are keyed by client IP; spoofedx-api-keyvalues do not create new buckets.5xxresponses always return a generic client message:"Internal server error". Inspect server logs and correlate byx-request-idfor full diagnostics.
npm install
cp .env.example .env
npm run devdoppler.config.tsis the canonical source for non-secret runtime settings.- Environment variables override typed settings at runtime.
- Required secrets remain in env:
API_KEY,PRIVATE_KEY(andREDIS_URLwhen needed). - The template object is type-checked via
DopplerTemplateConfigV1; config shape drift fails build/typecheck.
- Auction types:
multicurve(recommended default on V4-capable networks)dynamic(for higher-value assets that need maximally capital-efficient price discovery; supportsmigration.type="uniswapV2"ormigration.type="uniswapV4"in this API profile)static(Uniswap V3 static launch with lockable beneficiaries; compatibility fallback for networks without Uniswap V4 support)
- Multicurve initializer modes:
standard(implemented via scheduled initializer withstartTime=0)scheduled(startTimerequired)decay(startFee,durationSeconds, optionalstartTime)rehype(hook-based initializer config)
- Migration modes:
noOpfor multicurve/staticuniswapV2anduniswapV4for dynamicuniswapV3is not supported and returns501 MIGRATION_NOT_IMPLEMENTED
- Solana:
- create via
POST /v1/solana/launches - read launch account state via
GET /v1/solana/launches/:launchAddress POST /v1/launchesalso accepts Solana whennetworkissolanaDevnetorsolanaMainnetBeta- only
solanaDevnetis executable - only WSOL is supported as numeraire
- strict request shape; unsupported EVM-only fields are rejected
- create via
- Governance:
enabled=falseis the active profile, eg.noOp - Token allocation profile:
- Default: 100% of
totalSupplyis allocated to the multicurve market. - Optional: set
economics.tokensForSaleto allocate less to the market. - Remainder (
totalSupply - tokensForSale) is allocated to non-market allocation recipients. - Optional: set
economics.allocations.recipients(max 10 unique recipients) to split the non-market remainder.
- Default: 100% of
- Multicurve design reference: Doppler Multicurve whitepaper.
- Guidance: prefer
multicurvewhenever the target chain has Uniswap V4 support. Usestaticonly when V4 is unavailable.
- This API profile is at feature parity with the other Doppler launch APIs for the supported launch flows.
- EVM:
<chainId>:<txHash> - Solana: base58 launch PDA
Examples:
84532:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa8BD7a7kU4sASQ17S1X4Lw52dQWxwM8C2Y3jD7xA8fDzP
POST /v1/launches
x-api-key: <API_KEY>
Idempotency-Key: <UNIQUE_KEY>
content-type: application/json{
"chainId": 84532,
"userAddress": "0x1111111111111111111111111111111111111111",
"integrationAddress": "0x1111111111111111111111111111111111111111",
"tokenMetadata": {
"name": "My Token",
"symbol": "MTK",
"tokenURI": "ipfs://my-token-metadata"
},
"economics": {
"totalSupply": "1000000000000000000000000",
"tokensForSale": "800000000000000000000000",
"allocations": {
"recipients": [
{
"address": "0x1111111111111111111111111111111111111111",
"amount": "100000000000000000000000"
},
{
"address": "0x2222222222222222222222222222222222222222",
"amount": "100000000000000000000000"
}
],
"mode": "vest",
"durationSeconds": 7776000
}
},
"pricing": {
"numerairePriceUsd": 3000
},
"governance": {
"enabled": false,
"mode": "noOp"
},
"migration": {
"type": "noOp"
},
"auction": {
"type": "multicurve",
"curveConfig": {
"type": "preset",
"presets": ["low", "medium", "high"]
},
"initializer": {
"type": "standard"
}
}
}POST /v1/solana/launches
x-api-key: <API_KEY>
Idempotency-Key: <UNIQUE_KEY>
content-type: application/json{
"network": "devnet",
"tokenMetadata": {
"name": "My Solana Token",
"symbol": "MSOL",
"tokenURI": "ipfs://my-solana-token"
},
"economics": {
"totalSupply": "1000000000"
},
"pricing": {
"numerairePriceUsd": 150
},
"governance": false,
"migration": {
"type": "none"
},
"auction": {
"type": "xyk",
"curveConfig": {
"type": "range",
"marketCapStartUsd": 100,
"marketCapEndUsd": 1000
},
"swapFeeBps": 25,
"allowBuy": true,
"allowSell": true
}
}This repo currently supports two runtime modes:
-
standalone: one API instance owns its own local state and does not need cross-instance coordination. -
shared: multiple API instances can serve the same workload safely by coordinating through Redis. -
Single-instance / standalone (
DEPLOYMENT_MODE=standalone)- This is the default typed config mode and the simplest way to run the API.
IDEMPOTENCY_BACKEND=fileis the default.- Redis is optional.
- Good fit for one API instance, one signer, and a durable local filesystem.
- Redis is still recommended if you want stronger idempotency recovery around crashes/restarts.
-
Shared / multi-instance (
DEPLOYMENT_MODE=shared)- Intended for horizontally scaled or production-style shared deployments.
REDIS_URLis required.IDEMPOTENCY_BACKENDmust beredis.- Create endpoints always require
Idempotency-Key(IDEMPOTENCY_REQUIRE_KEY=trueis enforced). - Rate-limit state is Redis-backed for cross-replica consistency.
- Nonce submission uses a Redis-backed distributed signer lock for cross-replica coordination.
- Redis-backed idempotency writes an
in_progressmarker before tx submit to close crash/restart duplicate windows. - Retries against a stuck
in_progressmarker fail closed with409 IDEMPOTENCY_KEY_IN_DOUBT; verify launch status before attempting a new key. - Redis in-flight lock uses a heartbeat; tune
IDEMPOTENCY_REDIS_LOCK_TTL_MSto exceed max expected create duration.
NODE_ENV=production with no explicit DEPLOYMENT_MODE resolves to shared, so Redis becomes required in that case.
- Optional: single-instance / standalone deployments that use file-backed idempotency.
- Recommended: any deployment that wants stronger crash/restart recovery for create requests, even with one instance.
- Required: any shared deployment, multi-replica deployment, or any setup that explicitly sets
IDEMPOTENCY_BACKEND=redis.
Use this when you want deterministic, non-default market-cap bands instead of presets.
{
"auction": {
"type": "multicurve",
"curveConfig": {
"type": "ranges",
"fee": 15000,
"tickSpacing": 300,
"curves": [
{
"marketCapStartUsd": 100,
"marketCapEndUsd": 10000,
"numPositions": 11,
"sharesWad": "200000000000000000"
},
{
"marketCapStartUsd": 10000,
"marketCapEndUsd": 100000,
"numPositions": 11,
"sharesWad": "300000000000000000"
},
{
"marketCapStartUsd": 100000,
"marketCapEndUsd": "max",
"numPositions": 11,
"sharesWad": "500000000000000000"
}
]
}
}
}Use this only for the static fallback path.
{
"auction": {
"type": "static",
"curveConfig": {
"type": "range",
"marketCapStartUsd": 100,
"marketCapEndUsd": 100000
}
}
}Use this for the V4 dynamic flow. Dynamic exits/migrates when maxProceeds is reached, or at auction end when minProceeds is satisfied.
Dynamic is intended for assets with well-known value that benefit from maximally capital-efficient price discovery.
{
"migration": {
"type": "uniswapV2"
},
"auction": {
"type": "dynamic",
"curveConfig": {
"type": "range",
"marketCapStartUsd": 100,
"marketCapMinUsd": 50,
"minProceeds": "0.01",
"maxProceeds": "0.1",
"durationSeconds": 86400
}
}
}{
"launchId": "84532:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"chainId": 84532,
"txHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"statusUrl": "/v1/launches/84532:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"predicted": {
"tokenAddress": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"poolId": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"gasEstimate": "12500000"
},
"effectiveConfig": {
"tokensForSale": "800000000000000000000000",
"allocationAmount": "200000000000000000000000",
"allocationRecipient": "0x1111111111111111111111111111111111111111",
"allocationRecipients": [
{
"address": "0x1111111111111111111111111111111111111111",
"amount": "100000000000000000000000"
},
{
"address": "0x2222222222222222222222222222222222222222",
"amount": "100000000000000000000000"
}
],
"allocationLockMode": "vest",
"allocationLockDurationSeconds": 7776000,
"numeraireAddress": "0x4200000000000000000000000000000000000006",
"numerairePriceUsd": 3000,
"feeBeneficiariesSource": "default"
}
}{
"launchId": "8BD7a7kU4sASQ17S1X4Lw52dQWxwM8C2Y3jD7xA8fDzP",
"network": "solanaDevnet",
"signature": "5M7wVJf4t1A6sM97CG8PcHqx6LwH7qQ6B27vZ37h7uPj7m9Yx4mQnBn1HX9gD4FVyMPRZ4Jrped1ZSmHgkmHGW4J",
"explorerUrl": "https://explorer.solana.com/tx/5M7wVJf4t1A6sM97CG8PcHqx6LwH7qQ6B27vZ37h7uPj7m9Yx4mQnBn1HX9gD4FVyMPRZ4Jrped1ZSmHgkmHGW4J?cluster=devnet",
"predicted": {
"tokenAddress": "6QWeT6FpJrm8AF1btu6WH2k2Xhq6t5vbheKVfQavmeoZ",
"launchAuthorityAddress": "E7Ud4m8S7fC2YdUQdL7p9V2sRrMfQjQ9fA5spuR4T9gQ",
"launchFeeStateAddress": "F7Ud4m8S7fC2YdUQdL7p9V2sRrMfQjQ9fA5spuR4T9gR",
"baseVaultAddress": "9xQeWvG816bUx9EPjHmaT23yvVMHh2eHq9cYqB9Yg6xT",
"quoteVaultAddress": "J1veWvV6BF8L7rN8D66zCFAaj6MqFmoVoeAQMtkP8dwF"
},
"effectiveConfig": {
"tokensForSale": "1000000000",
"allocationAmount": "0",
"baseForDistribution": "0",
"baseForLiquidity": "0",
"allocationLockMode": "none",
"numeraireAddress": "So11111111111111111111111111111111111111112",
"numerairePriceUsd": 150,
"curveVirtualBase": "1000000000",
"curveVirtualQuote": "100000000",
"curveFeeBps": 25,
"swapFeeBps": 25,
"feeBeneficiariesSource": "default",
"feeBeneficiaries": [],
"allowBuy": true,
"allowSell": true,
"tokenDecimals": 6
}
}{
"launchId": "84532:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"chainId": 84532,
"txHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"status": "pending",
"confirmations": 0
}{
"launchId": "84532:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"chainId": 84532,
"txHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"status": "confirmed",
"confirmations": 2,
"result": {
"tokenAddress": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"poolOrHookAddress": "0xdddddddddddddddddddddddddddddddddddddddd",
"poolId": "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"blockNumber": "12345678"
}
}{
"launchId": "84532:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"chainId": 84532,
"txHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"status": "reverted",
"confirmations": 1,
"error": {
"code": "TX_REVERTED",
"message": "Transaction reverted on-chain"
}
}{
"defaultChainId": 84532,
"pricing": {
"enabled": true,
"provider": "coingecko"
},
"chains": [
{
"chainId": 84532,
"auctionTypes": ["multicurve", "dynamic"],
"multicurveInitializers": ["standard", "scheduled", "decay", "rehype"],
"migrationModes": ["noOp", "uniswapV2"],
"governanceModes": ["noOp", "default"],
"governanceEnabled": true
}
],
"solana": {
"enabled": true,
"supportedNetworks": ["solanaDevnet"],
"unsupportedNetworks": ["solanaMainnetBeta"],
"dedicatedRouteInputAliases": ["devnet", "mainnet-beta"],
"creationOnly": true,
"numeraireAddress": "So11111111111111111111111111111111111111112",
"priceResolutionModes": ["request", "fixed", "coingecko"]
}
}GET /health: process livenessGET /ready: dependency readiness (EVM chain RPC checks plus Solana readiness, requiresx-api-key)GET /metrics: service metrics snapshot (requiresx-api-key)- degraded readiness checks return a generic error string (
"dependency unavailable") to avoid leaking upstream internals
Example GET /health:
{ "status": "ok" }- Solana create-only rules:
- use
POST /v1/solana/launchesorPOST /v1/launcheswithnetwork: "solanaDevnet" | "solanaMainnetBeta" - short Solana aliases are accepted only on the dedicated route
launchIdis a launch PDA andstatusUrlpoints toGET /v1/solana/launches/:launchAddress- only WSOL is supported as numeraire
- Solana rejects unsupported EVM-only fields instead of ignoring them
- when
SOLANA_DEVNET_ALT_ADDRESSis set, launch creation reuses that address lookup table; if the signed transaction still exceeds Solana's packet limit, it creates a launch-specific lookup table and rebuilds the transaction
- use
- Solana
migration.type="none"launches use the initializer curve:- set
migration.supportCpmm=trueandmigration.minimumQuoteRaiseto register the launch with the CPMM migrator - all API-created launches use Doppler launch hook v1; CPMM migration registration is independent of hook features
- omit
economics.baseForDistributionandeconomics.baseForLiquidity, or set both to0, unlessmigration.supportCpmm=true - non-zero reserve fields return
422 SOLANA_INVALID_ECONOMICSunless CPMM migration support is enabled tokensForSale = totalSupply - baseForDistribution - baseForLiquidity
- set
- Solana
auction.cosignerGateconfigures Doppler-managed cosigning through Doppler launch hook v1:typemust be"cosigner"; callers cannot select or register a cosigner- the API resolves the canonical managed cosigner from the hook's on-chain config
- optional
expirysupportsmode: "disabled" | "unixTimestamp"; omitting it or usingdisabledcreates an indefinite gate, while timestamp mode requiresvalue - cosigner gating can be used with or without CPMM migration
- Solana
auction.dynamicFeeconfigures a fee schedule on Doppler launch hook v1:startFeeBpsandendFeeBpsare integer basis points between0and10000endFeeBpsmust be less than or equal tostartFeeBpsdurationSecondsis a non-negative integer string; it must be non-zero when the fee decaysstartingTimeis an optional non-negative Unix timestamp string; omit it or set it to"0"to start at launch creation- combine
auction.dynamicFeewithauction.cosignerGateto enable both features on the same hook - the effective swap fee is the greater of the dynamic schedule fee and
auction.swapFeeBps
- Solana auction fee input:
- prefer
auction.swapFeeBps;auction.curveFeeBpsremains accepted as a backward-compatible alias - if omitted, the API uses the on-chain initializer minimum swap fee
- request values must be within the on-chain initializer min/max swap-fee bounds
- prefer
- Solana fee beneficiaries:
- optional
feeBeneficiaries: [{ address, shareBps }]splits the post-protocol-fee share - custom lists support up to 8 unique addresses and
shareBpsmust sum to10000 - omitted beneficiaries default to the API payer when the protocol fee leaves a post-protocol share
- if the API payer is the initializer protocol beneficiary, callers must provide a non-protocol beneficiary list
- the initializer protocol beneficiary is rejected in request/default beneficiaries
- optional
- Multicurve initializer:
- default is
standard(implemented as scheduled withstartTime=0). scheduledrequiresauction.initializer.startTime.decayrequiresstartFeeanddurationSeconds(optionalstartTime).rehyperequires hook config and percent wad fields that sum to1e18.
- default is
- Multicurve curve selection:
- presets are convenient defaults.
- explicit
rangesare recommended when you need intentional market-cap bands instead of default tiers. - custom multicurve swap fees are supported via
curveConfig.fee(custom values supported; tick spacing can be derived or provided).
- Static launch curve config:
auction.type="static"requiresauction.curveConfig.curveConfig.type="preset"supportspreset: "low" | "medium" | "high".curveConfig.type="range"supports explicitmarketCapStartUsdandmarketCapEndUsd.- custom static fee input is supported via
curveConfig.fee, but Uniswap V3 still enforces valid V3 fee tiers onchain. - static launches always use lockable beneficiaries (request values or default 95% user / 5% protocol owner).
- use static only as a fallback when the target chain does not support Uniswap V4/multicurve.
- Dynamic launch curve config:
auction.type="dynamic"requiresauction.curveConfig.curveConfig.type="range"requires:marketCapStartUsdmarketCapMinUsdminProceeds(decimal string in numeraire units)maxProceeds(decimal string in numeraire units)
- optional:
durationSeconds,epochLengthSeconds,fee,tickSpacing,gamma,numPdSlugs - custom dynamic fees are supported via
curveConfig.fee. - dynamic launches require
migration.type="uniswapV2"ormigration.type="uniswapV4"in this API profile. - for
migration.type="uniswapV4", requestmigration.feeandmigration.tickSpacing. - for
migration.type="uniswapV4", streamable fee beneficiaries are derived fromfeeBeneficiaries(or the default 95/5 split). migration.type="uniswapV3"is reserved and currently returns501 MIGRATION_NOT_IMPLEMENTED.
- Percentage-based allocation is supported by converting percent to amount:
tokensForSale = totalSupply * salePercent / 100- Example: 20% sale means 80% non-market allocation.
integrationAddressis optional.governanceis binary at create time:- omitted/
false=> no governance trueor{ "enabled": true }=> default token-holder governance (OpenZeppelin Governor via protocol governance factory)
- omitted/
pricing.numerairePriceUsdoverrides provider pricing.- If auto-pricing is unavailable, caller must pass
pricing.numerairePriceUsd. - If
feeBeneficiariesis omitted, API applies default split:userAddress: 95%- protocol owner: 5%
feeBeneficiariesrequest constraints:- supports up to
10unique beneficiary addresses. - shares use WAD precision and must sum to
1e18(100%) when protocol owner is included. - if protocol owner is omitted, provided shares must sum to
95%(0.95e18) and API appends protocol owner at5%. - if protocol owner is provided, it must have at least
5%(WAD / 20).
- supports up to
See docs/mvp-launch.md for a concise MVP launch example and a full defaults-resolution table.
npm test
npm run test:static
npm run test:dynamic
npm run test:live
npm run test:live:static
npm run test:live:dynamic
npm run test:live:v2migration
npm run test:live:v4migration
npm run test:live:multicurve
npm run test:live:multicurve:defaults
npm run test:live:fees
npm run test:live:governance
npm run test:live:solana
npm run test:live:solana:devnet
npm run test:live:solana:defaults
npm run test:live:solana:fees
npm run test:live:solana:cpmm
npm run test:live:solana:no-migration
npm run test:live:solana:random
npm run test:live:solana:cosigner
npm run test:live:solana:dynamic-fee
npm run test:live:solana:failing
LIVE_TEST_VERBOSE=true npm run test:livetest:live performs real on-chain creation and verification when LIVE_TEST_ENABLE=true and funded credentials are configured.
By default, live output is concise (launch summary table). Set LIVE_TEST_VERBOSE=true for full per-launch parameter and verification tables.
Live launch tests run sequentially to avoid nonce conflicts from a single funded signer.
test:live remains the EVM baseline matrix; use test:live:solana or test:live:solana:devnet for the Solana devnet matrix. The Solana matrix covers supported parity with the Base Sepolia defaults, fee-beneficiary, reserve-split/CPMM, launches with no migration criteria, generic-route replay, randomized parameter paths, Doppler launch hook v1 launches with managed cosigner gating, and hook launches with scheduled dynamic fees. Governance, vesting/vault locks, and static/dynamic EVM auction engines are EVM-only.
Solana live tests require SOLANA_ENABLED=true, a funded SOLANA_KEYPAIR_PATH pointing to a Solana CLI keypair file, reachable SOLANA_DEVNET_RPC_URL / SOLANA_DEVNET_WS_URL, SOLANA_DEVNET_ALT_ADDRESS, and enough SOL for account creation; override the readiness estimate with LIVE_TEST_MIN_BALANCE_SOL, LIVE_TEST_ESTIMATED_TX_COST_SOL, and LIVE_TEST_ESTIMATED_OVERHEAD_SOL when needed. SOLANA_KEYPAIR remains available as an inline fallback, but do not set both payer variables. Solana RPC requests retry HTTP 429 responses with bounded exponential backoff. Live create verification retries transient SOLANA_NOT_READY and SOLANA_SUBMISSION_FAILED responses once after 10 seconds. The configured ALT remains the fast path; oversized launches fall back to a launch-specific ALT.
Tooling:
oxlint(oxlint.config.ts) — fast Rust-based ESLint replacement.oxfmt(oxfmt.config.ts) — fast Rust-based Prettier replacement.lefthook(lefthook.yml) — runs the formatter, linter, and typecheck against staged files before each commit.
Both oxlint.config.ts and oxfmt.config.ts use defineConfig from their respective packages for full type-checking and editor autocomplete. TypeScript configs require Node ≥22.18 (covered by .nvmrc / engines.node).
Scripts:
npm run lint # oxlint --deny-warnings
npm run lint:fix # oxlint --fix
npm run format # oxfmt (write)
npm run format:check # oxfmt --check
npm run fix # format + lint:fix
npm run check # format:check + lint + typecheck + testGit hooks (managed by lefthook):
pre-commit— formats staged files withoxfmt, runsoxlint --fix --deny-warningson staged JS/TS, restages fixed files, and runstsc --noEmitwhen TypeScript files are staged.pre-push— runsformat:check,lint,typecheck, andtest:unitin parallel.
Hooks install automatically via the prepare script when running npm install. To install manually run npx lefthook install. To bypass for a single commit, use git commit --no-verify (discouraged).