Implementation Plan - Meteora DLMM Copy Trading
Database Schema (Prisma)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model MonitoredWallet {
id String @id @default(cuid())
address String @unique
label String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
leaderPositions LeaderPosition[]
copyTrades CopyTrade[]
}
model LeaderPosition {
id String @id @default(cuid())
walletId String
wallet MonitoredWallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
positionAddress String @unique
lbPairAddress String
tokenXMint String
tokenYMint String
lowerBinId Int
upperBinId Int
strategyType String?
status String @default("OPEN")
openTxSignature String?
closeTxSignature String?
openedAt DateTime @default(now())
closedAt DateTime?
followerPosition FollowerPosition?
copyTrades CopyTrade[]
}
model FollowerPosition {
id String @id @default(cuid())
leaderPositionId String @unique
leaderPosition LeaderPosition @relation(fields: [leaderPositionId], references: [id], onDelete: Cascade)
positionAddress String @unique
lbPairAddress String
lowerBinId Int
upperBinId Int
status String @default("OPEN")
amountXDeposited String @default("0")
amountYDeposited String @default("0")
openedAt DateTime @default(now())
closedAt DateTime?
}
model CopyTrade {
id String @id @default(cuid())
walletId String
wallet MonitoredWallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
leaderPositionId String?
leaderPosition LeaderPosition? @relation(fields: [leaderPositionId], references: [id], onDelete: SetNull)
action String
leaderTxSignature String
followerTxSignature String?
lbPairAddress String
tokenXMint String?
tokenYMint String?
leaderAmountX String?
leaderAmountY String?
leaderMinBinId Int?
leaderMaxBinId Int?
leaderBpsToRemove Int?
followerAmountX String?
followerAmountY String?
status String @default("PENDING")
errorMessage String?
detectedAt DateTime @default(now())
executedAt DateTime?
}
model ActivityLog {
id String @id @default(cuid())
type String
message String
metadata String?
createdAt DateTime @default(now())
}
File Implementation Order
| # |
File |
Description |
| 1 |
prisma/schema.prisma |
DB schema (above) |
| 2 |
.env.local |
Env vars template |
| 3 |
src/lib/constants.ts |
Program ID, discriminator map |
| 4 |
src/lib/db/prisma.ts |
Prisma singleton |
| 5 |
src/lib/utils.ts |
retry, sleep, formatAddress |
| 6 |
src/lib/validation.ts |
Zod schemas for API inputs |
| 7 |
src/lib/solana/connection.ts |
Connection factory |
| 8 |
src/lib/meteora/types.ts |
Parsed operation types |
| 9 |
src/lib/solana/transaction-parser.ts |
Core: tx decoder using Anchor IDL |
| 10 |
src/lib/meteora/dlmm-client.ts |
DLMM SDK wrapper for copy execution |
| 11 |
src/lib/db/queries.ts |
DB query functions |
| 12 |
src/worker/heartbeat.ts |
WebSocket keepalive |
| 13 |
src/worker/monitor.ts |
logsSubscribe + tx fetching |
| 14 |
src/worker/position-tracker.ts |
Leader→follower mapping |
| 15 |
src/worker/executor.ts |
Copy trade execution |
| 16 |
src/worker/index.ts |
Worker entry point |
| 17-21 |
src/app/api/*/route.ts |
REST API routes |
| 22 |
package.json |
Add scripts: "worker": "tsx src/worker/index.ts" |
Directory Structure
src/
app/
layout.tsx
page.tsx
api/
wallets/route.ts
positions/route.ts
trades/route.ts
settings/route.ts
health/route.ts
lib/
constants.ts
utils.ts
validation.ts
db/
prisma.ts
queries.ts
solana/
connection.ts
transaction-parser.ts
meteora/
types.ts
dlmm-client.ts
worker/
index.ts
monitor.ts
heartbeat.ts
executor.ts
position-tracker.ts
prisma/
schema.prisma
.env.local
Key Code Patterns
Prisma Singleton
// src/lib/db/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
Transaction Parser Core Logic
import { BorshInstructionCoder } from '@coral-xyz/anchor';
import { IDL } from '@meteora-ag/dlmm';
const coder = new BorshInstructionCoder(IDL);
// For each instruction in tx:
// 1. Check programId matches DLMM
// 2. coder.decode(Buffer.from(ix.data)) → { name, data }
// 3. Map account indices to public keys (handle v0 lookup tables)
Worker Event Pipeline
monitor.on('meteoraTx', async (event) => {
const ops = parser.parse(event.tx);
for (const op of ops) {
if (autoCopyEnabled) await executor.executeCopy(op, event.walletAddress);
await tracker.track(op, event.walletAddress);
}
});
API Route Pattern
// Zod validation → Prisma query → NextResponse.json()
const schema = z.object({ address: z.string().refine(isValidSolanaAddress) });
Env Variables
DATABASE_URL="file:./dev.db"
RPC_ENDPOINT="https://api.mainnet-beta.solana.com"
WS_ENDPOINT="wss://api.mainnet-beta.solana.com"
FOLLOWER_PRIVATE_KEY="<base58>"
COPY_RATIO=1.0
MAX_POSITION_SIZE_SOL=10
SLIPPAGE_BPS=100
Verification
- Test transaction-parser with known Meteora tx signatures
- Start worker, add active LP address, check logs
- simulateTransaction for dry-run copy trades
- Small-amount live test: open→copy→close→copy-close
Commands
npx prisma db push # Create DB tables
npm run worker # Start monitor process
npm run dev # Start Next.js API
Next.js Config Notes
- Need webpack fallback for fs/net/tls/crypto on client side
- May need
serverComponentsExternalPackages: ['@coral-xyz/anchor', '@meteora-ag/dlmm']