Skip to main content

Overview

The Mouth backend consists of three main services:
  1. Mouth API — Core REST API serving the web app
  2. X Bot Service — Monitors X mentions and posts updates
  3. Resolution Engine — Handles bet resolution at expiration
All services share a single PostgreSQL database.

Database Schema

Core Tables

Indexes

API Endpoints

Auth

Bets

Users

Leaderboard

Challenges (X Bot Drafts)

Wallet

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.
When POST /bets receives an opponent X handle, the backend:
  1. Looks up the users table by x_handle
  2. If found → use wallet_address
  3. 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 users table with pre_registered = true
    • Use the new wallet_address
  4. Pass the resolved address to factory.createBet(..., opponent: walletAddress, ...)
When a pre-registered user eventually logs in via X OAuth, Privy recognizes their X ID and assigns the same wallet. The backend updates 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 the sendSponsored() relay function in services/tx-relayer.ts: These transactions are:
  • 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
See Identity & Wallets for the full authorization flow.

Admin Transactions (Resolver Account)

Resolution and finalization are performed by a dedicated backend wallet: These transactions:
  • Use a standard Viem WalletClient with 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

X Bot Service

The bot runs as a standalone Node.js service (x-bot/) that polls X for mentions of @Mouth_App, parses challenge intents using Claude Haiku, creates draft challenges in the database, and replies with a link to pvp.mouth.app/challenge/{id}. It writes to a dedicated draft_challenges table (no FK to users) and has no dependency on the backend API. See X Bot Service for the full technical architecture, database schema, LLM parsing, and deployment details.

Resolution Engine

Architecture

The resolution engine runs as a cron job (or event-driven worker) that:
  1. Every minute: Checks for bets where expiration_date <= NOW() and status = 'active'
  2. Categorizes each bet:
    • Auto-resolvable: Has oracle-compatible resolution criteria → Fetch data, resolve
    • Manual: Requires human review → Flag for Mouth team
  3. Submits resolution transaction to the smart contract
  4. 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)
The resolution criteria are stored as structured data in the bet’s description or as separate fields, allowing the engine to parse and evaluate automatically.