Overview
The Mouth backend consists of three main services:- Mouth API — Core REST API serving the web app
- X Bot Service — Monitors X mentions and posts updates
- Resolution Engine — Handles bet resolution at expiration
Database Schema
Core Tables
Indexes
API Endpoints
Auth
| Method | Path | Description |
|---|---|---|
POST | /auth/login | Authenticate via Privy (X OAuth) |
GET | /auth/me | Get current user profile |
Bets
| Method | Path | Description |
|---|---|---|
POST | /bets | Create a new bet (resolves opponent X handle → wallet, pre-generates if needed) |
GET | /bets | List bets (with filters: status, category, user) |
GET | /bets/:id | Get bet details |
POST | /bets/:id/accept | Accept a bet (triggers deposit) |
POST | /bets/:id/cancel | Cancel a pending bet (Challenger only) |
POST | /bets/:id/claim | Claim winnings (winner only) |
POST | /bets/:id/dispute | File a dispute |
POST | /bets/:id/resolve | Resolve a bet outcome (resolver only) |
POST | /bets/:id/finalize | Finalize after 48h dispute window (resolver only) |
Users
| Method | Path | Description |
|---|---|---|
GET | /users/:handle | Get user public profile |
GET | /users/:handle/bets | Get user’s bet history |
GET | /users/:handle/stats | Get user’s win/loss stats |
Leaderboard
| Method | Path | Description |
|---|---|---|
GET | /leaderboard | Get global leaderboard (sortable by profit, volume, streak) |
Wallet
| Method | Path | Description |
|---|---|---|
POST | /auth/withdraw | Withdraw USDC to external address (gasless, server-side via Privy) |
USDC balance is read directly on-chain by the frontend via viem — no backend endpoint needed. Wallet resolution is an internal service function (
resolveXHandleToWallet), not an API endpoint.POST /bets receives an opponent X handle, the backend:
- Looks up the
userstable byx_handle - If found → use
wallet_address - If not found:
- Check if user exists in Privy via
privy.users().getByTwitterUsername() - If not in Privy either, call Privy server SDK to create user + pre-generate embedded wallet
- Store in
userstable withpre_registered = true - Use the new
wallet_address
- Check if user exists in Privy via
- Pass the resolved address to
factory.createBet(..., opponent: walletAddress, ...)
pre_registered = false on first login.
See Identity & Wallets for details on Privy pre-generated wallets.
Transaction Signing
The backend uses two separate signing mechanisms depending on who initiates the transaction:User Transactions (Gasless via Privy)
All user-initiated on-chain operations go through thesendSponsored() relay function in services/tx-relayer.ts:
| Operation | Route | Relay function |
|---|---|---|
| Create bet | POST /bets | relayCreateBet() |
| Accept bet | POST /bets/:id/accept | relayDeposit() |
| Cancel bet | POST /bets/:id/cancel | relayWithdraw() |
| Claim winnings | POST /bets/:id/claim | relayClaim() |
| Withdraw USDC | POST /auth/withdraw | relayTransferUSDC() |
- Sent from the user’s embedded wallet (identified by
privyWalletId) - Gas-sponsored by Mouth via Privy’s infrastructure
- Authorized using a P256 authorization key registered as a key quorum in the Privy Dashboard, with session signer consent from the frontend
Admin Transactions (Resolver Account)
Resolution and finalization are performed by a dedicated backend wallet:| Operation | Route | Function |
|---|---|---|
| Resolve bet | POST /bets/:id/resolve | resolveBetOnChain() |
| Finalize bet | POST /bets/:id/finalize | finalizeBetOnChain() |
- Use a standard Viem
WalletClientwith the resolver’s private key (RESOLVER_PRIVATE_KEY) - Pay gas from the resolver wallet’s ETH balance
- Are protected by a wallet address check (only the resolver address can call these endpoints)
Environment Variables
| Variable | Used by | Description |
|---|---|---|
PRIVY_AUTHORIZATION_PRIVATE_KEY | User transactions | Base64-encoded PKCS8 P256 private key for Privy authorization |
RESOLVER_PRIVATE_KEY | Admin transactions | Hex-encoded secp256k1 private key for the resolver account |
X Bot Service
Architecture
The bot runs as a standalone Node.js service that:- Polls the X API v2 for mentions of
@MouthBet(or uses filtered stream if available) - Processes each mention to determine the action
- Calls the Mouth API to create bets or fetch data
- Posts replies on X with bet links, confirmations, and results
Mention Processing
Event-Driven Posts
The bot also subscribes to events from the Event Indexer:BetCreated→ Post bet link, tag opponentBetAccepted→ Post confirmationBetResolved→ Post resultBetCancelled→ Post cancellation (low priority)
Resolution Engine
Architecture
The resolution engine runs as a cron job (or event-driven worker) that:- Every minute: Checks for bets where
expiration_date <= NOW()andstatus = 'active' - Categorizes each bet:
- Auto-resolvable: Has oracle-compatible resolution criteria → Fetch data, resolve
- Manual: Requires human review → Flag for Mouth team
- Submits resolution transaction to the smart contract
- Starts the 48-hour dispute window
Oracle Integration
For auto-resolution, the engine queries:- Chainlink Price Feeds: Token prices (ETH, BTC, etc.)
- Pyth Network: Additional price data, faster updates
- On-chain queries: TVL, supply, contract state (via RPC calls)