Status: Full-stack complete Β· Deployed on Midl Staging Β· PRD v1.3 compliant
A pump.fun-style bonding curve launchpad on Bitcoin L2 (Midl Network). Create fungible tokens or NFT collections, trade with BTC, and watch prices rise with every purchase β all settled on Bitcoin.
In scope (v1):
- Buy-side and sell-side linear bonding curve β deterministic, on-chain pricing
- Bitcoin-native settlement β sign with Xverse wallet
- Trust-minimized via Midl validators and TSS vaults
- NFT collection launches with per-mint BTC pricing
- NFT secondary marketplace (list, buy, delist)
- Non-custodial social trading bot (@midllaunchbot on X)
Out of scope (per PRD):
- AMM or liquidity migration
- Anti-bot or fairness mechanisms
graph TD
A[User connects BTC wallet<br/>Xverse / Leather] --> B{Action}
B --> C[Create token launch]
B --> D[Buy tokens]
B --> E[Sell tokens]
B --> F[Launch NFT collection]
B --> G[Mint / trade NFTs]
B --> H[Social bot command on X]
C --> C1[Upload image + metadata<br/>to IPFS via Pinata]
C1 --> C2[Store pending metadata<br/>POST /api/pending-metadata]
C2 --> C3[Sign PSBT with LaunchFactory intent]
C3 --> C4[BTC tx broadcast + confirmed]
C4 --> C5[Midl EVM: LaunchFactory deploys<br/>LaunchToken + BondingCurvePrimaryMarket]
C5 --> C6[Indexer picks up LaunchCreated event<br/>writes Launch to SQLite + links IPFS metadata]
C6 --> C7[Redis pub/sub broadcasts launch_created]
C7 --> C8[WS server fans out to clients<br/>token appears on /launches]
D --> D1[Enter BTC amount + slippage tolerance]
D1 --> D2[Sign PSBT with buy intent]
D2 --> D3[BTC sent to TSS vault]
D3 --> D4[Bitcoin confirmation]
D4 --> D5[Midl EVM: BondingCurve.buy executes<br/>tokens minted to buyer EVM address]
D5 --> D6[Indexer picks up TokensPurchased event<br/>writes Purchase to SQLite]
D6 --> D7[Broadcasts tokens_purchased + price_update]
D7 --> D8[Frontend chart + live price update via WS]
D8 --> D9{Supply cap reached?}
D9 -->|Yes| D10[status = FINALIZED<br/>Broadcasts launch_finalized]
D9 -->|No| D8
E --> E1[Enter token amount to sell]
E1 --> E2[Sign PSBT with sell intent]
E2 --> E3[Midl EVM: BondingCurve.sell<br/>tokens burned Β· BTC released from vault]
E3 --> E4[Indexer picks up TokensSold event<br/>writes SELL record to SQLite]
E4 --> E5[Broadcasts tokens_sold + price_update]
F --> F1[Upload collection metadata to IPFS]
F1 --> F2[Sign PSBT with NftFactory intent]
F2 --> F3[Midl EVM: NftFactory deploys MidlNFT contract]
F3 --> F4[Collection appears on /launches NFT tab]
G --> G1[User mints NFT β BTC to TSS vault]
G1 --> G2[Midl EVM: MidlNFT.mint called]
G2 --> G3[POST /api/nft-launches/:addr/mints<br/>DB updated Β· nft_minted broadcast]
G3 --> G4[Secondary market via NftMarketplace contract<br/>list / buy / delist]
H --> H1[Bot parses X mention<br/>buy Β· sell Β· launch Β· portfolio]
H1 --> H2[Bot creates signing job<br/>POST /api/bot/jobs]
H2 --> H3[Bot replies with link to /bot/sign/:jobId]
H3 --> H4[User connects wallet Β· signs PSBT on sign page]
H4 --> H5[POST /api/bot/jobs/:jobId/execute]
H5 --> H6[BTC tx confirmed on-chain]
H6 --> H7[POST /api/bot/jobs/:jobId/confirm]
H7 --> H8[Bot polls /recently-confirmed<br/>posts receipt reply on X]
| Contract | Address |
|---|---|
| LaunchFactory | 0x25FE7EF91B9C4c79f1Ad46b7419d9db935ee24b1 |
| NftFactory | 0x06b81d8607E2C984513A763206B80048A9d584F8 |
| NftMarketplace | 0x9E312623C309d749Ceb50a954E0094502808288d |
LaunchFactory
- Blockscout: https://blockscout.staging.midl.xyz/address/0x25FE7EF91B9C4c79f1Ad46b7419d9db935ee24b1
- EVM tx: https://blockscout.staging.midl.xyz/tx/0x2cd78eb98510db3f657776994573af16d1fc16b7014a960a09b511835d8bb0e2
- BTC tx: https://mempool.staging.midl.xyz/tx/e74a36d440506c651d1ef72b2f92d108b85af24f5b32ac578ea822f5a420ef60
NftFactory
- Blockscout: https://blockscout.staging.midl.xyz/address/0x06b81d8607E2C984513A763206B80048A9d584F8
- EVM tx: https://blockscout.staging.midl.xyz/tx/0x2997b0fcb5228333064f7923fad704a89b077c18cafb68aae89735f8e880fb63
- BTC tx: https://mempool.staging.midl.xyz/tx/afb2b58aa7fb6a0896eb9bcf1fc32e9d484706ecc09de82da52aa70eea68e93b
NftMarketplace
- Blockscout: https://blockscout.staging.midl.xyz/address/0x9E312623C309d749Ceb50a954E0094502808288d
- EVM tx: https://blockscout.staging.midl.xyz/tx/0x4138c73037fd55e70902ba3b8d93b92537f347592c9416968395c9797658ac1a
- BTC tx: https://mempool.staging.midl.xyz/tx/ab19c2305f9072bb8e6eb82ca3ead941d0f80dd5cc660b414a6f339736610fe6
- Node.js 18+
- Redis instance
- Midl staging RPC access
npm install
npm test # Run 24 contract tests
npm run compile # Compile Solidity
npm run deploy:staging # Deploy to Midl stagingAfter deploy, deployments/env-update.txt contains ready-to-paste env snippets for backend and frontend.
cd backend
npm install
npx prisma migrate dev # Run DB migrations (SQLite)
npm run dev # Start API on :4000 + WebSocket on :8080cd frontend
cp .env.local.example .env.local # Fill in addresses + API URL
npm install
npm run dev # Start Next.js on :3002- Linear price:
P(s) = basePrice + s Γ priceIncrement - Closed-form buy math (Appendix A): Babylonian sqrt β no loops, O(1) gas
- Supply cap pre-check: buy() reverts before execution if purchase would exceed cap
- Gas cost: ~200β300k per purchase
- Slippage protection:
minTokensOutparameter
Parameter bounds (factory-enforced):
| Parameter | Min | Max |
|---|---|---|
| Base Price | 1,000 sats | 1,000,000 sats |
| Price Increment | 1 sat | 10,000 sats |
| Supply Cap | 1M tokens | 21M tokens |
- ERC721URIStorage + Ownable + ReentrancyGuard
- Fixed mint price in sats, per-wallet cap, max supply enforcement
mint()isnonReentrant; overpayment refunded to caller; creator receives exactmintPrice Γ quantityviacall- Token URI resolves to
ipfs://{collectionMetadataCID}/{tokenId}.json
- List, buy, delist secondary market for any MidlNFT collection
- Strict payment enforcement:
msg.value == listing.priceSats(no overpay) - Seller receives exact listing price via
call - ReentrancyGuard on
buy()
Supply cap enforcement Β· Monotonic price Β· Slippage protection Β· Parameter bounds Β· Event completeness (intentId) Β· Unauthorized mint protection Β· Gas O(1) Β· Admin scoping Β· Audit fix regressions
REST API served at http://localhost:4000.
Token launches:
| Endpoint | Description |
|---|---|
GET /api/launches |
List launches (filter by status, sort, paginate) |
GET /api/launches/trending |
Trending launches by score |
GET /api/launches/graduating |
Launches near 100% supply |
GET /api/launches/search |
Full-text search |
GET /api/launches/:address |
Launch detail |
GET /api/launches/:address/purchases |
Purchase history |
GET /api/launches/:address/price-history |
OHLC price points |
GET /api/launches/:address/chart |
Chart data |
GET /api/launches/:address/comments |
Comments |
POST /api/launches/:address/comments |
Post a comment |
PATCH /api/launches/:address/metadata |
Update metadata (CID, socials) |
POST /api/launches/:address/graduate |
Trigger graduation |
POST /api/pending-metadata |
Store pending metadata pre-launch |
NFT launches:
| Endpoint | Description |
|---|---|
GET /api/nft-launches |
NFT collection list |
GET /api/nft-launches/:address |
Collection detail |
GET /api/nft-launches/:address/mints |
Mint history |
POST /api/nft-launches/:address/mints |
Record a mint |
PATCH /api/nft-launches/:address/metadata |
Update collection metadata |
To prevent unauthorized modifications to the PostgreSQL database, all HTTP routes that update metadata or submit comments require a mathematically verifiable Bitcoin signature.
You do not need a session cookie or an API key. Instead, whenever you send a PATCH or a POST request, you must append an auth object to your JSON body containing a signature generated by your Bitcoin wallet (e.g. Unisat, Xverse, Leather).
Required Payload Format:
{
"auth": {
"address": "bc1q...",
"message": "I am the creator of [tokenAddress]",
"signature": "H8yAIxMVQfVcY4J..." // Base64 signature
}
}- Metadata Updates (
PATCH /api/launches/:address/metadata&PATCH /api/nft-launches/:address/metadata): The signature is recovered and its address must perfectly match thecreatorAddressof the Launch on record. - Comment Posting (
POST /api/launches/:address/comments): The signature is recovered and must match theauthorfield sending the comment.
You can test these endpoints without the frontend UI by using your wallet's built-in "Sign Message" tool.
- Open your Bitcoin wallet extension (e.g., Unisat).
- Look for "Sign Message" in the Settings.
- Type a message, e.g.,
"Update my token", and click Sign. - Copy the resulting Base64 Signature string.
curl -X PATCH http://localhost:4000/api/launches/YOUR_TOKEN_ADDRESS/metadata \
-H "Content-Type: application/json" \
-d '{
"twitterUrl": "https://x.com/newlink",
"auth": {
"address": "YOUR_WALLET_ADDRESS",
"message": "Update my token",
"signature": "PASTE_THE_SIGNATURE_HERE"
}
}'User:
| Endpoint | Description |
|---|---|
GET /api/user/:address/holdings |
Portfolio holdings + P&L |
GET /api/user/:address/activity |
Full activity (purchases, launches created, NFT mints) |
Platform:
| Endpoint | Description |
|---|---|
GET /api/activity |
Global activity feed (last 50 buys) |
GET /api/stats |
Platform stats |
GET /health |
Health check |
Bot:
| Endpoint | Description |
|---|---|
POST /api/bot/link-wallet |
Link X handle to EVM address |
GET /api/bot/wallet/:xHandle |
Look up wallet for X handle |
POST /api/bot/jobs |
Create signing job |
GET /api/bot/jobs/:jobId |
Get job status |
PATCH /api/bot/jobs/:jobId |
Update job |
POST /api/bot/jobs/:jobId/execute |
Execute job after signing |
POST /api/bot/jobs/:jobId/confirm |
Confirm job on-chain |
POST /api/bot/jobs/:jobId/mark-replied |
Mark bot reply sent |
POST /api/bot/jobs/:jobId/mark-expiry-replied |
Mark expiry reply sent |
GET /api/bot/jobs/recently-confirmed |
Recently confirmed jobs |
GET /api/bot/jobs/recently-expired |
Recently expired jobs |
POST /api/bot/jobs/expire-old |
Expire stale jobs |
GET /api/bot/stats |
Bot stats |
WebSocket (port 8080): broadcasts launch_created, tokens_purchased, tokens_sold, price_update, comment_posted, launch_finalized, nft_minted events.
| Route | Description |
|---|---|
/ |
Hero, platform stats, trending tokens, live activity feed, NFT teaser |
/launches |
Browse tokens + NFT collections β hero carousel, sort/filter, live search |
/launch/[address] |
Token detail: bonding curve chart, price history, buy/sell widget, trade history, comments |
/nft/[address] |
NFT collection: mint panel, your-NFTs gallery, secondary market listings |
/create |
Create token launch β IPFS metadata upload, curve parameters |
/launch-nft |
Create NFT collection launch |
/portfolio |
Holdings with unrealized P&L and full activity history |
/transactions |
Transaction lifecycle tracker |
/my-tokens |
Token launches created by connected wallet, with live stats |
/referral |
Referral link sharing (wallet address as referral ID) |
/bot/sign/[jobId] |
Wallet signing page for bot-initiated transactions |
/bot-demo |
Interactive bot demo |
/link-x |
Link X (Twitter) account to wallet address |
/how-it-works |
Protocol documentation β execution model, settlement mechanics, trust assumptions |
/terms |
Terms of service |
/privacy |
Privacy policy |
/risk-disclosure |
Risk disclosure |
Key features:
- Real-time buy feed via WebSocket
- Bonding curve SVG chart + price history line chart (5m / 1h / 4h / 1d intervals)
- One-click BTC purchase and sell via SatoshiKit (supports Xverse, Leather, and other BTC wallets)
- Live activity ticker in header
- Toast notifications for all user actions
- Canvas-confetti on token graduation (100% supply sold)
- Page transition animations
- Mobile-first with bottom navigation bar and sidebar
- IPFS/Pinata image upload in create forms
- First-visit disclaimer modal (localStorage-gated)
| Layer | Technology |
|---|---|
| Smart contracts | Solidity ^0.8.24, Hardhat, OpenZeppelin |
| Backend | Express.js, SQLite (local) / PostgreSQL (prod), Prisma ORM, Redis, Zod |
| Frontend | Next.js 14 (App Router), React 18, TypeScript |
| Styling | Tailwind CSS, CSS custom properties (design tokens) |
| Data fetching | TanStack Query v5 |
| Wallet | @midl/satoshi-kit, @midl/react, @midl/executor-react |
| Images | next/image with Pinata/IPFS remote patterns |
Backend (backend/.env):
DATABASE_URL="file:./prisma/dev.db"
MIDL_RPC_URL=https://rpc.staging.midl.xyz
FACTORY_ADDRESS=0x25FE7EF91B9C4c79f1Ad46b7419d9db935ee24b1
NFT_FACTORY_ADDRESS=0x06b81d8607E2C984513A763206B80048A9d584F8
NFT_MARKETPLACE_ADDRESS=0x9E312623C309d749Ceb50a954E0094502808288d
REDIS_URL=redis://localhost:6379
PORT=4000
WS_PORT=8080
CORS_ORIGIN=http://localhost:3002
START_BLOCK=0
POLL_INTERVAL_MS=5000
Frontend (frontend/.env.local):
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_WS_URL=ws://localhost:8080
NEXT_PUBLIC_APP_URL=http://localhost:3002
NEXT_PUBLIC_MIDL_RPC_URL=https://rpc.staging.midl.xyz
NEXT_PUBLIC_LAUNCH_FACTORY_ADDRESS=0x25FE7EF91B9C4c79f1Ad46b7419d9db935ee24b1
NEXT_PUBLIC_NFT_FACTORY_ADDRESS=0x06b81d8607E2C984513A763206B80048A9d584F8
NEXT_PUBLIC_NFT_MARKETPLACE_ADDRESS=0x9E312623C309d749Ceb50a954E0094502808288d
PINATA_API_KEY=
PINATA_SECRET=
See docs/DEPLOYMENT.md for full staging deployment guide.
# Deploy contracts (writes deployments/env-update.txt with addresses)
npm run deploy:staging
# Start backend
cd backend && npm run start
# Build + start frontend
cd frontend && npm run build && npm run start- Trust-minimized (not trustless): Midl validators control TSS vaults
- On-chain guarantees: supply cap, price monotonicity, parameter bounds, reentrancy protection β all enforced in Solidity
- Off-chain trust: frontend data accuracy, indexer availability, metadata correctness
- Bitcoin finality: settlement requires N BTC confirmations before Midl execution
- Audit fixes applied: supply cap pre-check in buy(), strict payment in NftMarketplace, nonReentrant on MidlNFT.mint(), overpayment refunds,
callinstead oftransferthroughout
- Sign up at developer.x.com
- Create a project and app
- Generate OAuth 1.0a keys (API Key, API Secret, Access Token, Access Token Secret)
- Purchase X API credits ($20 is enough to start β pay-per-use, no subscription)
- Enable auto-recharge at $5 threshold in Developer Console to avoid interruption
cp bot/.env.example bot/.env
# Fill in X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRETnpm run botAt 100 commands/month:
- 100 posts read x $0.005 = $0.50
- 100 replies x $0.010 = $1.00
- Total: ~$1.50/month
Most social trading bots are custodial: they hold your private keys and sign on your behalf. MidlLaunchBot is different.
The flow:
- You send a command on X
- Bot creates a signing request and replies with a link
- You tap the link β your Xverse wallet signs the actual Bitcoin PSBT
- Bot posts the on-chain proof once confirmed
Why this matters:
- Your keys never leave your wallet
- The bot cannot move your funds without your signature
- Every transaction is verifiable on Bitcoin mempool + Midl Blockscout
- If the bot is compromised, no user funds are at risk
"We built non-custodial social trading on a Bitcoin L2. The bot routes intent. The wallet signs. The chain proves."
All commands are fully integrated end-to-end β parsed from mentions, dispatched to signing flow, confirmed on-chain, proof posted as reply.
| Command | What happens |
|---|---|
@midllaunchbot buy $TOKEN 0.001 BTC |
Bot estimates tokens, sends signing link. User signs β BTC sent to bonding curve β tokens minted. Bot replies with proof. |
@midllaunchbot buy $TOKEN 100000 sats |
Same as above with sats denomination. |
@midllaunchbot sell $TOKEN 50% |
Bot calculates 50% of holdings, estimates BTC return, sends signing link. User signs β tokens burned β BTC returned. Bot replies with proof. |
@midllaunchbot sell $TOKEN all |
Sell 100% of holdings. |
@midllaunchbot launch MyToken ($MYT) |
Bot creates a launch job with defaults (1M supply, 1000 sat base price, 100 sat increment). User signs β token deployed via LaunchFactory. Bot replies with token address and trade link. |
@midllaunchbot portfolio |
Bot looks up holdings and replies with top 3 positions + total BTC value. No signing required. |
@midllaunchbot link |
Bot replies with link to /link-x to connect X handle β wallet address. No signing required. |
@midllaunchbot help |
Bot replies with command list. |
Bot launch defaults (when launching via bot β full config available on /create):
- Supply cap: 1,000,000 tokens
- Base price: 1,000 sats
- Price increment: 100 sats per token sold
- Creator fee: 0%
ISC