Core API service for Conviction Markets. This repo owns product and business logic for the Telegram and Farcaster clients.
Package manager: npm.
npm install
cp .env.example .env
npm run db:local:up
npm run db:generate
npm run db:push
npm run devThe API expects MongoDB. The default .env.example points to the local Docker MongoDB replica set. Run npm run db:local:up, then npm run db:push to sync Prisma indexes and collections before starting the server. Use MongoDB Atlas or another publicly reachable replica-set backed MongoDB deployment for production/Vercel.
npm run devstarts the Fastify server in watch mode.npm run buildruns the TypeScript compiler in check mode.npm run lintruns ESLint.npm run formatruns Prettier.npm run format:checkchecks formatting.npm run db:local:upstarts the local MongoDB replica-set container.npm run db:local:downstops the local MongoDB container.npm run db:generategenerates the Prisma client.npm run db:pushsyncs the Prisma schema to MongoDB. MongoDB does not use the old PostgreSQL migration files.npm run db:studioopens Prisma Studio.npm run markets:sync:polymarket -- --limit=50syncs real active Polymarket markets from Gamma.npm run contracts:buildcompiles the Foundry contracts incontracts/.npm run contracts:testruns the Foundry contract tests.npm run contracts:fmtformats the contract sources.
This service uses Prisma with MongoDB. After changing prisma/schema.prisma, sync the schema with MongoDB:
npm run db:pushPrisma Migrate is not used for this MongoDB setup. Numeric trading values are stored as validated decimal strings so the API does not lose precision through floating point storage.
The API uses api/index.ts as the Vercel serverless entry and routes every public request to the Fastify app from src/app.ts. Local development still uses src/index.ts and app.listen(). The Vercel install command includes dev dependencies for build-time TypeScript checks, and the build command runs Prisma client generation before TypeScript so cached builds use the current schema.
Production needs a real MongoDB connection string before deployment; mongodb://127.0.0.1:27017/... is only for local development and will not be reachable from Vercel.
Required Vercel environment variables:
DATABASE_URL=mongodb+srv://<username>:<password>@<cluster-url>/conviction_markets?retryWrites=true&w=majority
NODE_ENV=production
LOG_LEVEL=info
POLYMARKET_GAMMA_API_URL=https://gamma-api.polymarket.com
POLYMARKET_DATA_API_URL=https://data-api.polymarket.com
POLYMARKET_MARKETS_SYNC_LIMIT=50
POLYGON_RPC_URL=https://<production-polygon-rpc>
POLYMARKET_CREDENTIALS_ENCRYPTION_KEY=<32-byte-base64-or-64-character-hex-key>Deployment checklist:
vercel link --yes --project conviction-core-api
vercel env add DATABASE_URL production
vercel env add NODE_ENV production
vercel env add LOG_LEVEL production
vercel env add POLYMARKET_GAMMA_API_URL production
vercel env add POLYMARKET_DATA_API_URL production
vercel env add POLYMARKET_MARKETS_SYNC_LIMIT production
vercel env add POLYGON_RPC_URL production
vercel env add POLYMARKET_CREDENTIALS_ENCRYPTION_KEY production
npm run db:generate
npm run build
npm run lint
vercel --prodRun npm run db:push against the same production MongoDB database before beta testing writes. Then verify the public API is reachable without a Vercel login or bypass token:
curl https://<core-api-vercel-url>/health
curl https://<core-api-vercel-url>/marketsIf Vercel returns an authentication page, disable deployment protection for the public beta environment before wiring the URL into the Farcaster app. Farcaster clients cannot call a protected core API.
GET /healthreturns API health status.GET /users/:userId/polymarket/accountsreturns linked accounts and imported public position snapshots without exposing credentials.POST /users/:userId/polymarket/link-challengescreates a ten-minute, single-use ownership challenge.POST /users/:userId/polymarket/accountsverifies both wallet signatures and links an account without replacing the existing Conviction user or profile.POST /users/:userId/polymarket/accounts/:accountId/syncrefreshes current and closed Polymarket position snapshots.POST /users/:userId/polymarket/accounts/:accountId/unlink-challengescreates a signed unlink challenge.DELETE /users/:userId/polymarket/accounts/:accountIdconsumes the unlink challenge, removes stored credentials, and preserves Conviction history.GET /execution/capabilitiesreturns the current execution capability contract for clients.POST /execution/positions/:positionId/startrecords an execution attempt and blocks it while adapters/contracts are not live.GET /contracts/configlists stored contract deployments.POST /contracts/configstores a vault, adapter, or collateral-token deployment for a chain.GET /contracts/config/activelists active contract deployments, optionally filtered bychainId.POST /contracts/margin-intents/prepareprepares a real vault call payload from a pending margin position intent.PATCH /contracts/transactions/:idrecords wallet transaction hashes and transaction status updates.GET /positions/:positionId/contract-transactionslists contract transactions for one position.POST /social-accountscreates or fetches a real user from a Telegram or Farcaster identity.POST /trader-profilescreates or updates a trader profile for a real user.GET /trader-profiles/:idreturns one trader profile.GET /marketsreturns persisted market records. Until a real provider integration is added, this returns an empty list when no markets have been synced.GET /markets/:idreturns one persisted market record by internal market id.POST /signalscreates a trade signal against an existing trader profile and synced market.GET /signals/:idreturns one trade signal.GET /markets/:marketId/signalsreturns signals for one market.GET /trader-profiles/:traderProfileId/signalsreturns signals from one trader profile.POST /positionscreates a pending execution position intent for an existing user and market.GET /positions/:idreturns one position intent.GET /users/:userId/positionsreturns positions for one user.GET /trader-profiles/:traderProfileId/positionsreturns positions for the user behind one trader profile.POST /copy-tradescreates a pending execution copy intent against an existing source position.GET /users/:userId/copy-tradesreturns copy intents submitted by one follower user.GET /positions/:positionId/copy-tradesreturns copy intents for one source position.GET /leaderboardreturns trader stats calculated from real database records.GET /trader-profiles/:id/statsreturns stats for one trader profile.POST /omniston/quote-eventsrecords a quote-only Omniston attempt from Telegram or another client.GET /omniston/quote-eventsreturns recent Omniston quote attempts.GET /omniston/quote-summaryreturns quote totals, unique Telegram users, status counts, top pairs, and recent events.
The core API records quote-only Omniston usage from Telegram. This is analytics and grant reporting infrastructure; it does not build, sign, or submit swaps.
Record a quote result from a client:
curl -X POST http://localhost:3000/omniston/quote-events \
-H 'Content-Type: application/json' \
-d '{
"platform": "TELEGRAM",
"platformUserId": "7121972391",
"username": "OXbeach",
"fromAsset": "TON",
"toAsset": "USDT",
"amountUnits": "1000000000",
"status": "QUOTED",
"inputUnits": "1000000000",
"outputUnits": "3500000",
"settlement": "swap",
"resolverName": "example-resolver"
}'Read recent events and summary metrics:
curl http://localhost:3000/omniston/quote-events?limit=20
curl http://localhost:3000/omniston/quote-summaryStatuses are REQUESTED, QUOTED, NO_QUOTE, FAILED, TIMEOUT, and DISABLED. Telegram records terminal statuses for accepted /quote attempts so totals map cleanly to user quote activity.
Market data must come from real provider integrations. The Polymarket provider reads public market records from the Gamma API and persists them through the shared market sync service. The sync path does not create fallback markets, placeholders, demo markets, or hardcoded trading data.
Configure the provider in .env:
POLYMARKET_GAMMA_API_URL=https://gamma-api.polymarket.com
POLYMARKET_MARKETS_SYNC_LIMIT=50Sync real active Polymarket markets for local development or admin use:
npm run markets:sync:polymarket -- --limit=50If Polymarket or MongoDB is unavailable, the command exits with POLYMARKET_SYNC_FAILED and does not insert placeholder records.
Trade signals are expressions of thesis or intent. Creating a signal does not create a position, calculate PnL, or imply execution. The referenced trader profile and market must already exist in the database.
Create a signal:
curl -X POST http://localhost:3000/signals \
-H 'Content-Type: application/json' \
-d '{
"traderProfileId": "existing-trader-profile-id",
"marketId": "existing-market-id",
"side": "YES",
"thesis": "Market thesis based on the trader's real view.",
"convictionLevel": 75,
"source": "WEB"
}'Read signals:
curl http://localhost:3000/signals/:id
curl http://localhost:3000/markets/:marketId/signals
curl http://localhost:3000/trader-profiles/:traderProfileId/signalsThe API supports intent-first execution records for beta testing. Clients may create margin position intents with executionMode=MARGIN, EVM chain metadata, wallet address, collateral, and leverage. These records stay PENDING_EXECUTION. Starting execution creates an ExecutionAttempt with BLOCKED status until real contracts, vault liquidity, liquidation rules, and provider adapters are live.
Current capability discovery:
curl http://localhost:3000/execution/capabilitiesCreate a margin intent and record the blocked attempt:
curl -X POST http://localhost:3000/positions \
-H 'Content-Type: application/json' \
-d '{
"userId": "existing-user-id",
"marketId": "existing-market-id",
"side": "YES",
"quantity": "10",
"executionMode": "MARGIN",
"chainId": 8453,
"walletAddress": "0x0000000000000000000000000000000000000000",
"leverageMultiplier": "3",
"marginCollateral": "25"
}'
curl -X POST http://localhost:3000/execution/positions/:positionId/startThis does not execute a trade, submit an order, create PnL, or mark a position as executed.
Contracts live in contracts/ inside this API repo. The first scaffold is ConvictionVault, an ERC20 collateral vault that records margin intents and locks collateral while the intent is pending or executed. It supports owner-managed collateral policies, pause controls, emergency cancellation, basic account risk accounting, and authorized operators for future real execution adapters.
This scaffold does not make margin execution live. The API must keep marginExecutionEnabled=false and leverageEnabled=false until contracts are deployed, funded with real liquidity, monitored, and connected to real execution adapters. Configuring CONVICTION_VAULT_ADDRESS or CONVICTION_EXECUTION_ADAPTER_ADDRESS only exposes deployment metadata in GET /execution/capabilities; it does not mark positions as executed.
Contract commands:
npm run contracts:build
npm run contracts:testOptional contract env values:
CONVICTION_VAULT_ADDRESS=
CONVICTION_EXECUTION_ADAPTER_ADDRESS=
CONVICTION_VAULT_OWNER=
BASE_SEPOLIA_RPC_URL=
BASE_MAINNET_RPC_URL=
DEPLOYER_PRIVATE_KEY=Start with Base Sepolia for deployment tests. Do not use production funds or claim execution support until adapter confirmation, liquidation and close-position flows, collateral policy review, monitoring, and operational controls are implemented.
The API syncs the deployed testnet vault and Circle testnet USDC deployment records on startup. The connected testnet chains are:
| Chain | Vault | Collateral |
|---|---|---|
| Base Sepolia | 0xfeBCb5b9bCD90904aa9d6100eDff504A606494E3 |
0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| Ethereum Sepolia | 0xB1dA85e3867f926f8ED3Aa5954Ab7dd8Db29f605 |
0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 |
| Arbitrum Sepolia | 0xd53cec8fF2d49Fa5Fe3C6dE5408ce996f0A0858c |
0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d |
You can still upsert or deactivate deployment records manually with POST /contracts/config when a chain changes.
Prepare the wallet flow from a real pending margin position in order:
curl -X POST http://localhost:3000/contracts/collateral-approvals/prepare \
-H 'Content-Type: application/json' \
-d '{ "positionId": "existing-margin-position-id" }'
curl -X POST http://localhost:3000/contracts/deposits/prepare \
-H 'Content-Type: application/json' \
-d '{ "positionId": "existing-margin-position-id" }'Prepare the margin-intent contract call after the collateral approval and deposit transactions confirm:
curl -X POST http://localhost:3000/contracts/margin-intents/prepare \
-H 'Content-Type: application/json' \
-d '{
"positionId": "existing-margin-position-id",
"maxSlippageBps": 100
}'Each prepare endpoint creates a PREPARED contract transaction record and returns the target contract, ABI fragment, and arguments for the client wallet. Recording a submitted hash updates transaction tracking only; it does not mark the position as executed. Execution requires adapter confirmation and real venue/on-chain evidence.
curl -X PATCH http://localhost:3000/contracts/transactions/:id \
-H 'Content-Type: application/json' \
-d '{
"transactionHash": "0x...",
"status": "SUBMITTED"
}'Positions and copy records are intent records until a real execution adapter is added. New records are created with PENDING_EXECUTION. They must not be marked EXECUTED unless a real adapter confirms execution. Failed or cancelled attempts can use FAILED or CANCELLED once execution handling exists.
Execution fields such as averageEntryPrice, executedQuantity, executionPrice, resultingPositionId, and openedAt stay null when there is no confirmed execution. The API does not calculate PnL.
When a synced market has real price fields, the API stores an observedMarketPrice snapshot from the market record at intent creation time. If no real market price is available, observedMarketPrice, observedMarketPriceSource, and observedMarketPriceAt are returned as null.
Create a position intent:
curl -X POST http://localhost:3000/positions \
-H 'Content-Type: application/json' \
-d '{
"userId": "existing-user-id",
"marketId": "existing-market-id",
"side": "YES",
"quantity": "10.00000000"
}'Create a copy intent:
curl -X POST http://localhost:3000/copy-trades \
-H 'Content-Type: application/json' \
-d '{
"followerId": "existing-user-id",
"sourcePositionId": "existing-source-position-id",
"requestedQuantity": "5.00000000"
}'Read positions and copy intents:
curl http://localhost:3000/positions/:id
curl http://localhost:3000/users/:userId/positions
curl http://localhost:3000/trader-profiles/:traderProfileId/positions
curl http://localhost:3000/users/:userId/copy-trades
curl http://localhost:3000/positions/:positionId/copy-tradesStats are calculated from persisted records only:
numberOfSignalscounts realTradeSignalrows for a trader profile.numberOfCopyIntentscounts realCopyTraderows submitted against positions owned by the trader profile's user.copiedVolumesumsrequestedQuantityfrom those submitted copy intents.executedCopiedVolumesumsexecutedQuantityonly for copy intents withEXECUTEDstatus; it returnsnullwhen there are no executed copy intents.realizedPnlreturnsnulluntil real execution and close data exists in the database.
The leaderboard does not invent win rate, PnL, trader performance, or copied volume. Entries are sorted by real copy intent count, copied volume, then signal count.
Read stats:
curl http://localhost:3000/leaderboard
curl http://localhost:3000/trader-profiles/:id/statsThis demo flow uses real local records created through the API. Replace every placeholder with a real local operator value. Do not seed fake users, fake traders, fake markets, fake positions, or fake trade history.
Start local MongoDB, sync the schema, and run the API:
npm install
cp .env.example .env
npm run db:local:up
npm run db:generate
npm run db:push
npm run devSync real markets from Polymarket. If the provider is unavailable, stop here and keep the market empty state visible.
npm run markets:sync:polymarket -- --limit=10
curl http://localhost:3000/marketsCreate or fetch a real Telegram user record. Use your own Telegram numeric ID and username, or run /start in the Telegram bot once it points at this API.
curl -X POST http://localhost:3000/social-accounts \
-H 'Content-Type: application/json' \
-d '{
"platform": "TELEGRAM",
"platformUserId": "<your-real-telegram-user-id>",
"username": "<your-real-telegram-username>",
"displayName": "<your-real-display-name>",
"profileUrl": "https://t.me/<your-real-telegram-username>"
}'Create or update a trader profile for that real user. Use the returned user.id from the previous response.
curl -X POST http://localhost:3000/trader-profiles \
-H 'Content-Type: application/json' \
-d '{
"userId": "<real-user-id>",
"handle": "<real-trader-handle>",
"bio": "Local demo profile for a real operator."
}'Create a signal against a synced market. Use a real market.id from GET /markets and the real traderProfile.id from the previous response.
curl -X POST http://localhost:3000/signals \
-H 'Content-Type: application/json' \
-d '{
"traderProfileId": "<real-trader-profile-id>",
"marketId": "<real-synced-market-id>",
"side": "YES",
"thesis": "My real thesis for this market.",
"convictionLevel": 75,
"source": "WEB"
}'Create a pending position intent from real user input. This is not execution and does not create PnL.
curl -X POST http://localhost:3000/positions \
-H 'Content-Type: application/json' \
-d '{
"userId": "<real-user-id>",
"marketId": "<real-synced-market-id>",
"side": "YES",
"quantity": "10.00000000"
}'Create a second real user, then submit a copy intent against the source position. The response should remain PENDING_EXECUTION until a real execution adapter exists.
curl -X POST http://localhost:3000/copy-trades \
-H 'Content-Type: application/json' \
-d '{
"followerId": "<real-follower-user-id>",
"sourcePositionId": "<real-source-position-id>",
"requestedQuantity": "5.00000000"
}'Check the demo read endpoints:
curl http://localhost:3000/health
curl http://localhost:3000/leaderboard
curl http://localhost:3000/trader-profiles/<real-trader-profile-id>/stats
curl http://localhost:3000/positions/<real-source-position-id>/copy-tradesDemo script:
- Run core API on
http://localhost:3000. - Sync real Polymarket markets or show the empty market state if sync is unavailable.
- Start Telegram with
CORE_API_URL=http://localhost:3000. - Start Farcaster/web with the same
CORE_API_URL=http://localhost:3000. - Create or fetch a real Telegram user through
/startorPOST /social-accounts. - Create a real trader profile for that user.
- Create a trade signal against a real synced market.
- Open the Farcaster signal page and share the Mini App card.
- Submit a copy intent from another real user.
- Confirm leaderboard and stats update only from the recorded signal/copy-intent rows.
prismakeeps the MongoDB Prisma schema.src/configkeeps environment validation and runtime config.src/routeskeeps HTTP route modules.src/servicesis reserved for service modules.src/libkeeps shared helpers such as Prisma, responses, and errors.src/pluginskeeps Fastify plugins and cross-cutting handlers.testsis reserved for test coverage.
Do not add fake users, fake traders, fake markets, fake positions, fake PnL, or demo trade history. Records should come from real user input or real integrations when those integrations are added.