LIME Backend is the off-chain service layer for LIME Markets. It exposes the API used by the frontend for signed order intake, order book reads, and future matching workflows while relying on the on-chain LIME Programs for custody, Positions, Resolution, Settlement, Claims, and Refunds.
The current backend slice focuses on Signed Limit Order intake. It verifies wallet authorization, checks on-chain Market and Collateral state, persists Open Orders, and reserves collateral in Postgres so the Matching Engine has a canonical off-chain state to build on.
LIME Backend owns the canonical off-chain state for:
- Signed Orders
- Reserved Collateral
- Order Book state
- future Fills, Trades, and Trade Execution idempotency
Supabase may still support frontend-owned app data, but it is not the source of truth for Matching Engine correctness. The frontend should talk to this backend when reading or writing matching state.
Implemented:
- NestJS API under
/v1 - Prisma/Postgres persistence
- Signed Limit Order validation for
lime.signed-limit-order.v1 - Solana RPC reads for Market and User Collateral state
- Open Order persistence
- Reserved Collateral tracking
- order book and order read APIs
- unit and e2e tests
Out of scope for this first slice:
- matching execution
- cancellations
- fills and trade production
- WebSockets
- partial fills
- on-chain Trade Execution
- post-Resolution Settlement
- Node.js and TypeScript
- NestJS
- Prisma
- Postgres
- Zod
@solana/web3.jstweetnacl- Jest and Supertest
Read CONTEXT.md before changing matching behavior. That file defines the project vocabulary and the boundaries between Backend, Matching Engine, Orders, Positions, Trade Execution, and Settlement.
Key rules:
- A Signed Order is wallet-authorized order intent, not a backend-only record.
- A Buy Order increases Long exposure when filled.
- A Sell Order increases Short exposure when filled.
- Reserved Collateral is tracked by the backend and prevents double spending across open orders.
- Trade Execution is distinct from post-Resolution Settlement.
- Settlement is not produced by the Backend or Matching Engine.
Architectural decisions live in docs/adr:
- Backend owns matching state in Postgres.
- The MVP backend uses Node, TypeScript, NestJS, and Postgres.
- Signed Order V1 verification uses deterministic JSON serialization compatible with the current frontend serializer.
Install dependencies:
npm installCreate a local environment file:
cp .env.example .envSet the required values in .env:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/lime_backend"
SOLANA_CLUSTER="devnet"
SOLANA_CHAIN_ID="solana-devnet"
SOLANA_RPC_URL="https://api.devnet.solana.com"
LIME_MARKET_PROGRAM_ID="<market-program-id>"
LIME_VAULT_PROGRAM_ID="<vault-program-id>"
PORT="3001"
CORS_ORIGIN="http://localhost:5173"Start local Postgres:
docker compose up -dGenerate Prisma client and run migrations:
npm run prisma:generate
npm run prisma:migrateRun the API in development mode:
npm run devThe API listens on PORT and uses the /v1 global prefix. With the default config, the base URL is:
http://localhost:3001/v1
npm run dev # start NestJS in watch mode
npm run build # compile TypeScript
npm run start # run compiled app from dist
npm run test # run unit tests
npm run test:e2e # run e2e tests
npm run prisma:generate # generate Prisma client
npm run prisma:migrate # run development migrationsPOST /v1/orders/limitRequest body:
{
"payload": {
"schema": "lime.signed-limit-order.v1",
"market_id": "42",
"owner": "<solana-public-key>",
"action": "buy",
"exposure": "long",
"quantity": 25,
"limit_price_scaled": 425000,
"expiry_ts": 1800000000,
"nonce": "nonce-1",
"chain_id": "solana-devnet"
},
"message": "{\"schema\":\"lime.signed-limit-order.v1\",...}",
"signature": "<base64-ed25519-signature>"
}Accepted response:
{
"status": "accepted",
"orderId": "<sha256-order-id>"
}Rejected response:
{
"status": "rejected",
"reason": "INVALID_SIGNATURE",
"message": "Wallet signature does not authorize this signed order."
}Expected rejection reasons include:
INVALID_SCHEMAINVALID_MESSAGEINVALID_SIGNATUREINVALID_CHAININVALID_MARKETMARKET_NOT_ACTIVEORDER_EXPIREDNONCE_ALREADY_USEDACTION_EXPOSURE_MISMATCHINVALID_PRICEINVALID_QUANTITYINSUFFICIENT_AVAILABLE_COLLATERALINTERNAL_ERROR
GET /v1/markets/:marketId/orderbookReturns aggregated bid and ask levels for Open Orders in the given Market.
GET /v1/markets/:marketId/ordersReturns Open Orders for a Market.
GET /v1/owners/:owner/ordersReturns Open Orders for a wallet owner.
The backend verifies Signed Limit Orders by reconstructing the canonical V1 JSON message from the submitted payload, comparing it to the submitted message, and verifying the wallet signature over that exact UTF-8 string.
Validation includes:
- schema must be
lime.signed-limit-order.v1 chain_idmust matchSOLANA_CHAIN_IDbuymust pair withlongsellmust pair withshortexpiry_tsmust be in the futuremarket_idmust fit in a Solanau64- owner must be a valid Solana public key
- signature must be a valid Ed25519 signature from the owner
- price must be between 0 and 1 as a scaled integer
- quantity must be positive after scaling
Required Collateral:
- Buy:
Quantity * Price - Sell:
Quantity - (Quantity * Price)
The backend checks Available Collateral on-chain and subtracts already Reserved Collateral recorded in Postgres before accepting a new order.
The initial Prisma schema stores:
orders: accepted Signed Orders, keyed by deterministic Order IDreserved_collateral: collateral reserved for Open Orders
Replay protection is enforced with a unique constraint on:
chainId + marketId + owner + nonce