A ready-to-deploy AI persona bot for Discord, built on Dadida.
git clone https://github.com/aiwhiteteam/dadida-starter my-bot
cd my-bot
npm install
cp .env.example .env # fill in your tokensThe bot connects over the Discord Gateway (WebSocket) — an outbound, long-lived connection. You don't need a public URL, webhook, or ngrok; it runs fine from your laptop or any host behind NAT.
-
Create the bot. Go to the Discord Developer Portal → New Application → Bot (left sidebar) → Reset Token, then copy the token into
DISCORD_TOKENin your.env. -
Enable the Message Content Intent (required). On the same Bot page, under Privileged Gateway Intents, turn on MESSAGE CONTENT INTENT.
⚠️ Without this, incoming messagecontentarrives empty and the classifier/responder have nothing to work with. -
Invite the bot to your server. Go to OAuth2 → URL Generator.
- Under Scopes, check only
bot(ignoreidentify,guilds,applications.commands, etc. — you don't need them). - The moment you check
bot, a Bot Permissions panel appears below the scopes list. Scroll down to it and check the permissions in the table below. (Scopes and Bot Permissions are two separate panels — the permissions checkboxes won't show up untilbotis checked.) - These permissions get encoded into the invite URL, so set them before inviting. Copy the generated URL at the bottom, open it in a browser, and authorize it for your server.
Permission Required? Used for View Channels✅ Yes Receiving messages Send Messages✅ Yes Replies and escalation messages Read Message History✅ Recommended Conversation context / history tool Moderate Members🟡 Only for moderation Timing out (muting) users Moderate Membersis Discord's "timeout" permission, used by the moderator plugin's mute action. To mute, the bot's role must also sit above the members it manages (Server Settings → Roles). Skip it if you only want chat replies. - Under Scopes, check only
-
(Optional) Get a channel ID. In the Discord client, enable Settings → Advanced → Developer Mode, then right-click a channel → Copy ID and put it in
LISTEN_CHANNEL_IDS. Leave it empty to listen to all channels. -
Run it.
npm run dev— the bot is connected once it shows up online in your server.
npm run devnpm run build
npm startThe bot runs as a long-lived worker process (it holds a Discord WebSocket / gateway connection). There is no HTTP server and no health-check endpoint — the platform just needs to keep one process alive. Run exactly one instance: a second instance would open a duplicate gateway connection and double-reply.
Deploy to Railway, which builds with Nixpacks (no Dockerfile needed):
- Fork this repo
- Create a Railway project → connect your repo
- Set environment variables (see
.env.example— at minimumDISCORD_TOKENandOPENAI_API_KEY). Env vars are injected by Railway, so no.envfile is needed —npm startuses--env-file-if-existsand skips it when absent. - Railway auto-detects (Nixpacks) and runs:
- Build:
npm run build - Start:
npm start
- Build:
- Deploy — Railway runs it as a worker process and monitors process health directly (no health check needed).
- Keep the service at 1 replica (a second instance would duplicate the gateway connection and double-reply).
The bot stores message history in a SQLite file at ./data/messages.db, which
powers conversation context and the search_history tool. Railway's filesystem is
ephemeral and wiped on every redeploy — the bot still runs, it just starts each
deploy with an empty memory. To keep history across restarts, attach a volume:
- Open the project and select your bot service.
- Right-click the service → Attach Volume (or Settings → Volumes → Add Volume).
- Set the mount path to
/app/dataand create it — Railway redeploys with the volume attached. - Keep the service at 1 replica (volumes can't attach to multi-replica services).
By default the bot only records messages it receives while running. To load a
channel's existing history into the store (so recentMessages and search_history
can see it), run the backfill script:
npm run build
npm run backfill -- <channelId> [--start <ISO>] [--end <ISO>] [--max <n>]--start/--end— only messages in this time range (ISO dates, e.g.2025-01-01or2025-01-01T00:00:00Z). Default: from the beginning up to now.--max— cap how many messages to scan (safety limit).- Uses
DISCORD_TOKEN(same bot — needs Read Message History + the Message Content Intent) and writes to the same./data/messages.db.
It's idempotent (keyed on the Discord message id, so re-running never duplicates) and each message keeps its original timestamp, not the time of the backfill. Tip: overlap the window with when the bot went live — dedup handles the overlap, so you get no gaps and no duplicates.
You pass ISO dates for
--start/--end; the script converts them to Discord snowflakes internally, so you never compute snowflakes by hand.
The <channelId> is a Discord snowflake. To copy it:
- Discord → Settings → Advanced → Developer Mode (turn on).
- Right-click the channel in the sidebar → Copy Channel ID.
(Same trick on a message → Copy Message ID if you ever need a specific message's snowflake; note its creation time is encoded in the id.)
Set MEM0_API_KEY in .env to enable a parallel Mem0 semantic memory layer:
MEM0_API_KEY=...When enabled, new Discord messages are written to both SQLite and Mem0. The
search_history tool then returns merged candidates from relaxed SQLite keyword
search and Mem0 semantic search. SQLite remains the source of truth for raw
message history.
To backfill existing SQLite history into Mem0:
npm run build
npm run mem0:backfill -- [--channel <channelId>] [--start <ISO>] [--end <ISO>] [--max <n>] [--batch <n>]--channel— only process SQLite rows from this Discord channel. By default, all channels in./data/messages.dbare processed.--start/--end— only messages in this time range.--max— cap how many SQLite rows to process.--batch— SQLite page size, default 100.
Mem0 backfill is idempotent at the script layer. Successful submissions are
recorded in a local mem0_sync table inside ./data/messages.db, scoped by the
Mem0 base URL, app ID, agent ID, and Discord message ID. Re-running the same
range skips rows that were already submitted to the same Mem0 target.
Mem0's add API processes memories asynchronously, so the ledger records accepted
submissions rather than waiting for final memory extraction completion.
Optional Mem0 env vars:
MEM0_APP_ID— defaultdadida-starter.MEM0_AGENT_ID— defaultinvestor-persona.MEM0_RERANK=false— disable Mem0 reranking.MEM0_BASE_URL— defaulthttps://api.mem0.ai.
Run it inside the deployed service so it writes to the mounted volume — running
it on your laptop would write to your local ./data, not the Railway volume.
npm i -g @railway/cli # once
railway login # once
railway link # select your project + service
railway ssh # shell into the running container
# now inside the container:
npm run backfill -- <channelId> --start 2025-01-01dist/backfill.js is already built during the Railway deploy, and DISCORD_TOKEN
is already in the service environment, so the command above just works.
personas/identity.md— who your persona is (name, role, vibe)personas/soul.md— how your persona behaves (voice, style, boundaries)knowledge/— domain knowledge your persona can referenceplugins/— classifier, responder, moderator logic
├── index.ts ← entry point
├── plugins/
│ ├── investing-classifier.ts ← what topics to engage with
│ ├── investor-reply.ts ← persona response logic
│ └── moderator.ts ← warn / mute / escalate
├── personas/
│ ├── identity.md ← the business card
│ ├── soul.md ← the personality
│ └── moderator-soul.md ← moderator personality
├── knowledge/
│ ├── trading-rules.md
│ └── current-views.md
└── .env.example