> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mouth.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Backend Architecture

> API, database, and service design for the Mouth platform.

## 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

```sql theme={null}
-- Users table: maps X identity to wallet
users (
  id               UUID PRIMARY KEY,
  privy_id         TEXT UNIQUE NOT NULL,
  x_handle         TEXT NOT NULL,
  x_id             TEXT UNIQUE NOT NULL,        -- immutable X numeric ID
  avatar_url       TEXT,                        -- X profile picture, updated on each login
  wallet_address   TEXT NOT NULL,               -- embedded wallet on Base
  privy_wallet_id  TEXT,                        -- Privy embedded wallet ID (used for server-side gasless transactions)
  factory_approved BOOLEAN DEFAULT FALSE,       -- true once user has infinite USDC approval to factory
  pre_registered   BOOLEAN DEFAULT FALSE,       -- true if wallet was pre-generated before user login
  created_at       TIMESTAMP DEFAULT NOW(),
  updated_at       TIMESTAMP DEFAULT NOW()
)

-- Bets table: off-chain metadata + on-chain reference
bets (
  id                UUID PRIMARY KEY,
  contract_address  TEXT UNIQUE,               -- on-chain bet contract address
  challenger_id     UUID REFERENCES users(id),
  opponent_id       UUID REFERENCES users(id), -- NULL if open bet
  title             TEXT NOT NULL,
  description       TEXT,
  category          TEXT DEFAULT 'Other',
  amount            NUMERIC NOT NULL,          -- USDC amount per side
  status            TEXT NOT NULL,             -- pending, active, resolving, resolved, cancelled, expired
  outcome           TEXT,                      -- challenger_wins, opponent_wins, draw, NULL
  expiration_date   TIMESTAMP NOT NULL,
  acceptance_deadline TIMESTAMP,
  draft_challenge_id UUID,                     -- link to draft if created from X bot
  tweet_url         TEXT,                      -- original tweet context
  created_at        TIMESTAMP DEFAULT NOW(),
  resolved_at       TIMESTAMP,
  claimed_at        TIMESTAMP
)

-- Disputes table
disputes (
  id              UUID PRIMARY KEY,
  bet_id          UUID REFERENCES bets(id),
  filed_by        UUID REFERENCES users(id),
  reason          TEXT NOT NULL,
  evidence_url    TEXT,
  status          TEXT NOT NULL,               -- open, reviewing, resolved
  resolution_note TEXT,
  created_at      TIMESTAMP DEFAULT NOW(),
  resolved_at     TIMESTAMP
)

-- Activity log for leaderboard and profiles
activity_log (
  id          UUID PRIMARY KEY,
  user_id     UUID REFERENCES users(id),
  bet_id      UUID REFERENCES bets(id),
  action      TEXT NOT NULL,                   -- created, accepted, won, lost, claimed, cancelled
  amount      NUMERIC,
  created_at  TIMESTAMP DEFAULT NOW()
)
```

### Indexes

```sql theme={null}
CREATE INDEX idx_bets_status ON bets(status);
CREATE INDEX idx_bets_challenger ON bets(challenger_id);
CREATE INDEX idx_bets_opponent ON bets(opponent_id);
CREATE INDEX idx_bets_expiration ON bets(expiration_date);
CREATE INDEX idx_activity_user ON activity_log(user_id);
CREATE INDEX idx_users_x_handle ON users(x_handle);
CREATE INDEX idx_users_x_id ON users(x_id);
```

## 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) |

### Challenges (X Bot Drafts)

| Method | Path                      | Description                                                            |
| ------ | ------------------------- | ---------------------------------------------------------------------- |
| `GET`  | `/challenges/:id`         | Get draft challenge details (public, includes avatars and linked bet)  |
| `POST` | `/challenges/:id/confirm` | Challenger finalizes draft → creates real bet on-chain (auth required) |

### Wallet

| Method | Path             | Description                                                        |
| ------ | ---------------- | ------------------------------------------------------------------ |
| `POST` | `/auth/withdraw` | Withdraw USDC to external address (gasless, server-side via Privy) |

<Note>
  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.
</Note>

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](/architecture/identity-and-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`:

| 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()` |

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](/architecture/identity-and-wallets#authorization-how-the-server-signs-for-user-wallets) for the full authorization flow.

### 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()` |

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

| 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

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](/architecture/x-bot) 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.
