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.
Overview
The X bot (@Mouth_App) is a standalone Node.js service that monitors Twitter/X for mentions, parses challenge intents using an LLM, creates draft challenges in the database, and replies with a link to pvp.mouth.app/challenge/{id}.
It is purely a distribution layer — it does not handle funds, deploy contracts, or resolve challenges. All on-chain operations happen when users click the link and interact with the web app.
Service Architecture
The bot runs as an independent process, separate from the backend API. Both share the same Neon PostgreSQL database.Key Dependencies
| Package | Purpose |
|---|---|
twitter-api-v2 | X API v2 client — read mentions, post replies |
ai + @ai-sdk/anthropic | AI SDK for LLM-based tweet parsing |
drizzle-orm + postgres | Database ORM (shared Neon instance) |
dotenv | Environment variable loading |
tsx | TypeScript execution (dev mode) |
Mention Polling
The bot uses the User Mention Timeline endpoint (GET /2/users/:id/mentions) to fetch new mentions every 30 seconds.
Polling Flow
- On startup, fetch the bot’s user ID from
@Mouth_Appusername - Poll mentions with
since_idto only get new tweets - For each mention:
- Dedup check: skip if
tweet_idalready exists indraft_challenges - LLM parse: extract challenge parameters from tweet text
- Confidence filter: skip if LLM confidence < 0.5 (not a real challenge)
- Create draft: insert into
draft_challengeswith parsed data - Post reply: respond with challenge link
- Dedup check: skip if
Rate Limits
The X API free/pay-per-use tier provides:- Mention reads: polled every 30 seconds, 100 tweets per request
- Tweet writes: replies posted per processed challenge
since_id in memory to avoid re-fetching old mentions. On restart, it re-fetches recent mentions but deduplicates via the database.
LLM Tweet Parser
The parser uses Claude Haiku 4.5 (claude-haiku-4-5-20251001) via the Anthropic API to extract structured challenge data from natural language tweets.
Input
A raw tweet like:Output
Extraction Rules
| Field | Extraction Logic |
|---|---|
title | Core prediction/claim, cleaned of @mentions |
amount | Dollar amount from $, USD, USDC patterns. Min $10, default 0 |
opponentUsername | @mentioned handle that isn’t @Mouth_App. Null = open challenge |
expirationDescription | Natural language date (“end of April”, “by Friday”, “Q2”) |
odds | Challenger’s perceived probability (10-90). Default 50 |
confidence | 0-1 score. Below 0.5 = tweet is a question, joke, or unrelated mention |
Cost
Claude Haiku 4.5 is the cheapest model available. Each tweet parse costs approximately $0.001-0.002 — negligible even at high volume.Database: draft_challenges
Draft challenges are stored in a separate table from bets, with no foreign keys to users. This is intentional:
- The bot doesn’t know if the challenger or opponent are registered users
- Draft only stores X handles as strings — identity resolution happens when users log in
- A draft becomes a real
betonly after the challenger confirms and deposits USDC
Indexes
Draft Lifecycle
Reply Templates
The bot posts different replies depending on the event:Challenge Created (PvP)
Challenge Created (Open)
Challenge Accepted
Challenge Resolved
X API Authentication
The bot uses OAuth 1.0a User Context for posting replies (writing as@Mouth_App) and Bearer Token for reading mentions.
| Credential | Purpose |
|---|---|
X_BEARER_TOKEN | App-level read access (mention timeline) |
X_CONSUMER_KEY + X_CONSUMER_SECRET | App identity (API Key pair) |
X_ACCESS_TOKEN + X_ACCESS_TOKEN_SECRET | User context for @Mouth_App (read + write) |
The X app must have Read and Write permissions enabled, and the app type must be set to Web App, Automated App or Bot (confidential client). After changing permissions, the Access Token must be regenerated.
Environment Variables
| Variable | Required | Description |
|---|---|---|
X_BEARER_TOKEN | Yes | X API Bearer Token for reading mentions |
X_CONSUMER_KEY | Yes | X API Consumer Key |
X_CONSUMER_SECRET | Yes | X API Consumer Secret |
X_ACCESS_TOKEN | Yes | X API Access Token (Read+Write) |
X_ACCESS_TOKEN_SECRET | Yes | X API Access Token Secret |
X_BOT_USERNAME | No | Bot account handle (default: Mouth_App) |
POLL_INTERVAL_MS | No | Polling interval in ms (default: 30000) |
ANTHROPIC_API_KEY | Yes | Anthropic API key for Claude Haiku |
DATABASE_URL | Yes | Neon PostgreSQL connection string |
APP_URL | No | Base URL for challenge links (default: https://pvp.mouth.app) |
Deployment
The bot is deployed as a standalone service on Railway, separate from the backend API. Both connect to the same Neon PostgreSQL database.Why Standalone?
- Isolation: if the bot crashes or hits X rate limits, the API stays up
- Independent scaling: bot is I/O-bound (X API + LLM), API is request-driven
- Simpler deploys: bot can be restarted without affecting live users
Draft → Bet Conversion
When a user clicks the challenge link (pvp.mouth.app/challenge/{id}), the flow is:
- Frontend fetches draft data from
GET /challenges/:id - Challenger logs in with X via Privy → backend recognizes the X handle
- Review: challenger sees pre-filled form (title, amount, odds, expiration) — can edit
- Confirm + Deposit: challenger deposits USDC → backend creates the real
betentry, deploys smart contract, and updatesdraft_challenges.bet_id+draft_challenges.status = 'confirmed' - Opponent clicks same link → sees “Accept Challenge” → deposits → bet goes active
- Bot posts update on X: “It’s ON!”
The
draft_challenges table has no foreign keys to users. Identity resolution (X handle → wallet address) only happens when the user logs in and the backend calls resolveXHandleToWallet(). See Identity & Wallets for details on pre-generated wallets.