A modern, self-hosted Magic: The Gathering deck builder with Mana Pool integration, price monitoring, cart optimization, and multi-user support.
- Intuitive Deck Builder β real-time search with autocomplete as you type
- Multiple Boards β mainboard, sideboard, and maybeboard support
- Commander Support β mark commanders, color identity displayed automatically
- Multiple Printings β choose specific card art, set, and foil version per card
- Printing Optimizer β automatically find the cheapest or most thematic set for your entire deck
- Deck Statistics β live mana curve, type distribution, color breakdown, and estimated value
- Format Support β Commander, Standard, Modern, Pioneer, Legacy, Vintage, Pauper
- Drag & Drop β reorder cards between boards
- Layout Views β full card, compact, and ultra-compact views
- Cart Optimizer β find the cheapest combination of sellers for your deck or shopping list across four strategies:
- Lowest Total Price β minimize spend
- Fewest Packages β minimize shipping costs
- Balanced β balance between price and convenience
- Gathered Shipping Only β sellers offering combined shipping
- Deck Validator β check any deck for format legality via Mana Pool (Commander, Standard, Modern, Pioneer, Legacy, Vintage, Pauper)
- Buy Links β every card in the deck builder and shopping list has a direct "Buy on Mana Pool" link
- Price Monitoring β watch card prices and get notified when they hit your target
- Price Watches β set a target price and condition (NM/LP/MP/HP/DM) for any card
- Automated Checks β prices polled on a configurable schedule (default: every 6 hours)
- Push Notifications β instant alerts via ntfy when a card hits your target price
- Price History Charts β beautiful Chart.js line graphs showing price over time with your target threshold overlaid
- Smart Alerting β won't re-notify more than once per 24 hours for the same watch
- Watch Management β pause, resume, expire, and delete watches from a single dashboard
- Card Autocomplete β instant card name search when adding a watch
- Cross-Deck Aggregation β see every card you need across all your decks in one list, organized by set
- Mana Pool Optimizer β run the optimizer directly from the shopping list to get a ready-to-buy cart
- Per-Card Buy Links β one-click links to Mana Pool search for every card
- Filters β price range, rarity, color, set search, and budget mode
- Session Tracking β mark cards as "found" or "skip" during an LGS visit
- Export β export the full shopping list as formatted text
- Advanced Card Browser β filter by color, type, CMC range, set, subtype, and rarity
- Inventory Management β track cards you own with quantity per printing
- Owned Card Badges β see which cards in your decks you already own
- Card Detail Modal β all printings with prices, artist, set, legality, and rulings
- Hover Previews β full card image on hover throughout the app
- Import from Moxfield, MTG Arena, MTGO, and plain text formats
- Export to Moxfield (with set codes), Arena, MTGO, and plain text
- Smart Parser β auto-detects set codes in brackets or parentheses
- Shared Deck Import β one-click import of any publicly shared deck
- Public Links β generate a unique read-only URL for any deck
- No Account Required β anyone with the link can view the deck
- Import Shared Decks β viewers can save a copy to their own account
- Multi-User β each user has their own decks, inventory, and price watches
- JWT Authentication β secure token-based auth with refresh tokens
- API Keys β generate named API keys for external integrations
- Gravatar Support β profile pictures via Gravatar with colorful initials fallback
- User Statistics β deck count, card count, shared decks, and API keys at a glance
- User Management β view, promote/demote, and delete users
- Backup & Restore β export and restore all user data as JSON
- Database Sync β manually trigger MTGJSON card data and pricing refresh
- Auto-Sync β card data updated daily at 3 AM automatically
- Safe Reimport β
FORCE_REIMPORTclears card data but preserves all user decks via UUIDs
mkdir -p deck-lotus-data
cat > docker-compose.yml << 'EOF'
services:
deck-lotus:
image: ghcr.io/madeofpendletonwool/deck-lotus:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- JWT_SECRET=replace-with-a-long-random-secret
- ADMIN_USERNAME=admin
- ADMIN_EMAIL=admin@example.com
- ADMIN_PASSWORD=changeme123
# Optional but recommended β enables price monitoring & cart optimizer
- MANAPOOL_API_TOKEN=
# Optional β enables push notifications for price alerts
- NTFY_TOPIC=
volumes:
- ./deck-lotus-data:/app/data
restart: unless-stopped
EOF
docker compose up -dApp runs at http://localhost:3000. Generate a secure JWT secret with:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"git clone https://github.com/madeofpendletonwool/deck-lotus.git
cd deck-lotus
cp .env.example .env
# Edit .env β set JWT_SECRET at minimum
docker compose up -d --build| Variable | Default | Required | Description |
|---|---|---|---|
JWT_SECRET |
β | Yes | Secret for signing JWT tokens. Use a long random string. |
PORT |
3000 |
No | Port the server listens on |
NODE_ENV |
development |
No | production or development |
DATABASE_PATH |
./data/deck-lotus.db |
No | Path to the SQLite database |
MANAPOOL_USER_EMAIL |
β | No | Your Mana Pool account email address. Required alongside the token. |
MANAPOOL_API_TOKEN |
β | No | Enables price monitoring, cart optimizer, and deck validator. Generate at manapool.com/seller/settings/integrations |
TCGPLAYER_CLIENT_ID |
β | No | TCGPlayer API fallback (keys no longer publicly issued) |
TCGPLAYER_CLIENT_SECRET |
β | No | TCGPlayer API fallback |
NTFY_URL |
https://ntfy.sh |
No | ntfy server URL for push notifications |
NTFY_TOPIC |
β | No | ntfy topic name for price alert push notifications |
PRICE_CHECK_SCHEDULE |
0 */6 * * * |
No | Cron expression for price check frequency |
ADMIN_USERNAME |
β | No | Username for initial admin account |
ADMIN_EMAIL |
β | No | Email for initial admin account |
ADMIN_PASSWORD |
β | No | Password for initial admin account |
JWT_EXPIRES_IN |
7d |
No | Access token lifetime |
JWT_REFRESH_EXPIRES_IN |
30d |
No | Refresh token lifetime |
MTGJSON_URL |
(auto) | No | Custom MTGJSON download URL |
FORCE_REIMPORT |
false |
No | Set true to clear and reimport all card data on next startup. User decks are preserved. |
Mana Pool is the preferred price source and unlocks the most powerful features. To enable it:
- Create a Mana Pool account at manapool.com
- Go to Seller Settings β Integrations
- Generate an API access token
- Set both
MANAPOOL_USER_EMAIL(your account email) andMANAPOOL_API_TOKENin your.envordocker-compose.yml
With the token set you get:
- Price monitoring that actually works
- Cart optimizer in the buy modal and shopping list
- Deck legality validator
- Per-card buy links throughout the app
- Install the ntfy app on your phone, or use the web UI
- Pick a unique topic name (e.g.
deck-lotus-alerts-abc123) - Set
NTFY_TOPICto that name - Subscribe to the same topic in the ntfy app
You'll receive a push notification whenever a watched card price drops to or below your target.
On every startup, Deck Lotus checks whether an admin user exists:
- Admin exists β nothing happens, server starts normally
- No admin exists + env vars set β creates or promotes
ADMIN_USERNAMEto admin - No admin exists + no env vars β auto-generates credentials and prints them to the console once
Auto-generated credentials look like this:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AUTO-GENERATED ADMIN CREDENTIALS β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β Username: admin β
β Email: admin@localhost β
β Password: a1b2c3d4e5f6... β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ£
β β οΈ SAVE THESE CREDENTIALS NOW! β
β They will not be shown again. β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Change the password immediately after first login.
- Go to My Decks β New Deck
- Name your deck and optionally set a format
- Search for cards β autocomplete shows results as you type
- Click a card to select a printing and add it
- Use +/- or the quantity field to adjust counts
- Switch between Mainboard, Sideboard, and Maybeboard tabs
- Right-click any card for the actions menu (move board, set as commander, buy on Mana Pool, remove)
- Click Save
- Open a deck β click Buy Deck
- The Mana Pool Optimizer tab opens by default:
- Choose a strategy (Lowest Price, Fewest Packages, Balanced, or Gathered Shipping Only)
- Click Optimize Cart β results show a per-seller breakdown with prices
- Click View on Mana Pool to complete the purchase
- Switch to Quick Export to copy the deck list for Mana Pool or TCGplayer mass entry
- Open a deck β look for Validate Deck in the sidebar stats panel
- Select a format (Commander, Standard, Modern, etc.)
- Click Validate β results show legality status, your commander, color identity, and any violations
- Go to Price Watch in the nav
- Click Add Watch
- Start typing a card name β autocomplete suggests matching cards
- Set a target price and condition
- Optionally set an expiry date and notes
- Click Save Watch
Prices are checked on the configured schedule. When a price hits your target you get a push notification (if ntfy is configured) and the watch shows a green "Price Hit!" badge.
Click the chart icon on any watch to see the full price history graph with your target threshold.
- Go to Shopping List
- Select the decks you want to shop for
- Use filters to narrow down by price, rarity, or color
- Click Optimize Cart to run the Mana Pool optimizer across your entire list
- Click the Mana Pool button on any individual card to search for it directly
- Mark cards as Found during an LGS visit to track progress
- Go to My Decks β Import Deck
- Enter a deck name and format
- Paste your deck list β any of these formats work:
4 Lightning Bolt(Arena / MTGO)4 Counterspell [DMR](Moxfield with set code)1 Black Lotus (LEA)(set code in parentheses)
- Click Import Deck
Generate API keys for external integrations:
- Go to Settings β Generate New API Key
- Enter a name β save the key (shown once only)
Use in requests:
curl -H "X-API-Key: your-api-key" http://localhost:3000/api/cards/search?q=lightningAll endpoints require authentication via Authorization: Bearer <token> or X-API-Key: <key> unless noted.
| Method | Path | Description |
|---|---|---|
POST |
/api/auth/register |
Register a new account |
POST |
/api/auth/login |
Login, returns JWT |
GET |
/api/auth/me |
Current user profile |
GET |
/api/auth/stats |
User statistics |
GET |
/api/auth/api-keys |
List API keys |
POST |
/api/auth/api-keys |
Create API key |
DELETE |
/api/auth/api-keys/:id |
Revoke API key |
| Method | Path | Description |
|---|---|---|
GET |
/api/cards/search?q=&limit= |
Autocomplete search |
GET |
/api/cards/browse |
Filtered card browser (name, colors, type, cmc, sets, sort, page) |
GET |
/api/cards/:id |
Card details |
GET |
/api/cards/:id/printings |
All printings with prices |
POST |
/api/cards/:id/owned |
Toggle card ownership |
| Method | Path | Description |
|---|---|---|
GET |
/api/decks |
List decks |
POST |
/api/decks |
Create deck |
GET |
/api/decks/:id |
Get deck |
PUT |
/api/decks/:id |
Update deck |
DELETE |
/api/decks/:id |
Delete deck |
POST |
/api/decks/:id/cards |
Add card |
PUT |
/api/decks/:id/cards/:cardId |
Update card quantity / board |
DELETE |
/api/decks/:id/cards/:cardId |
Remove card |
GET |
/api/decks/:id/stats |
Mana curve, type distribution, colors |
GET |
/api/decks/:id/price |
Deck total estimated value |
POST |
/api/decks/import |
Import from text |
POST |
/api/decks/:id/share |
Create public share link |
DELETE |
/api/decks/:id/share |
Remove share link |
GET |
/api/decks/share/:token |
View shared deck (public) |
POST |
/api/decks/share/:token/import |
Import shared deck |
| Method | Path | Description |
|---|---|---|
GET |
/api/price-monitoring/status |
Check if price source and ntfy are configured |
GET |
/api/price-monitoring |
List all price watches |
POST |
/api/price-monitoring |
Create price watch |
PUT |
/api/price-monitoring/:id |
Update watch (price, condition, active state) |
DELETE |
/api/price-monitoring/:id |
Delete watch |
GET |
/api/price-monitoring/:id/history |
Price check history |
POST |
/api/price-monitoring/check-now |
Trigger immediate check of all active watches |
| Method | Path | Description |
|---|---|---|
GET |
/api/manapool/status |
Check if MANAPOOL_API_TOKEN is configured |
POST |
/api/manapool/optimize |
Cart optimizer β body: { items, model } |
POST |
/api/manapool/validate-deck |
Deck validator β body: { decklist, format } |
POST |
/api/manapool/card-info |
Card info lookup β body: { names } |
| Model | Description |
|---|---|
lowest_price |
Minimize total spend |
fewest_packages |
Minimize number of sellers (saves shipping) |
balanced |
Balance between price and packages |
gathered_shipping_only |
Sellers offering combined shipping only |
POST /api/manapool/optimize
Authorization: Bearer <token>
Content-Type: application/json
{
"model": "fewest_packages",
"items": [
{ "name": "Sol Ring", "quantity": 1 },
{ "name": "Arcane Signet", "quantity": 1 },
{ "name": "Command Tower", "quantity": 1 }
]
}POST /api/manapool/validate-deck
Authorization: Bearer <token>
Content-Type: application/json
{
"format": "commander",
"decklist": "1 Atraxa, Praetors' Voice\n1 Sol Ring\n..."
}All admin endpoints require admin privileges.
| Method | Path | Description |
|---|---|---|
GET |
/api/admin/users |
List all users |
PUT |
/api/admin/users/:id |
Update user (promote/demote admin) |
DELETE |
/api/admin/users/:id |
Delete user and all their data |
POST |
/api/admin/users/:id/reset-password |
Reset user password |
POST |
/api/admin/sync |
Trigger MTGJSON sync |
GET |
/api/admin/sync-status |
Last sync timestamp |
POST |
/api/admin/backup |
Create data backup |
POST |
/api/admin/restore |
Restore from backup |
GET |
/api/admin/backups |
List saved backup files |
deck-lotus/
βββ src/
β βββ db/
β β βββ migrations/ # 017 database migrations
β β βββ connection.js
β β βββ index.js
β βββ middleware/
β β βββ auth.js # JWT + API key auth
β β βββ errorHandler.js
β βββ routes/
β β βββ auth.js
β β βββ cards.js
β β βββ decks.js
β β βββ sets.js
β β βββ shopping.js
β β βββ inventory.js
β β βββ priceMonitoring.js
β β βββ manapool.js # Mana Pool proxy routes
β β βββ admin.js
β βββ services/
β βββ authService.js
β βββ cardService.js
β βββ deckService.js
β βββ pricingService.js # Local deck price calculation
β βββ manaPoolService.js # Mana Pool API integration
β βββ tcgplayerService.js # TCGPlayer fallback
β βββ priceMonitoringService.js
β βββ notificationService.js # ntfy integration
β βββ syncService.js
βββ client/
β βββ src/
β β βββ components/
β β β βββ deckBuilder.js # Deck builder + optimizer + validator
β β β βββ cards.js
β β β βββ shopping.js # Shopping list + Mana Pool optimizer
β β β βββ inventory.js
β β β βββ priceMonitoring.js # Price watches + Chart.js history charts
β β β βββ settings.js
β β β βββ auth.js
β β βββ services/api.js # API client
β β βββ utils/
β β βββ main.js
β βββ index.html
β βββ package.json
βββ scripts/
β βββ import-mtgjson.js
βββ Dockerfile
βββ docker-compose.yml
βββ .env.example
βββ package.json
Backend β Node.js, Express, SQLite (better-sqlite3), JWT, bcrypt, node-cron
Frontend β Vanilla JavaScript (ES6+), Vite, Chart.js (price history charts), Phosphor Icons
Data β MTGJSON (card data, synced weekly), Mana Pool API (live prices, optimizer, validator)
Admins can export and restore all user data from Settings β Backup & Restore.
Backups include: users, decks, deck cards, API keys, and share tokens. Card database data is excluded β it auto-imports from MTGJSON.
Restores can merge (keeps existing data, adds backup data) or overwrite (replaces everything). Deck integrity is maintained via stable UUIDs.
The Dockerfile uses a three-stage build to keep the final image small (~150 MB):
- Frontend builder β runs
npm run buildin the client directory - Backend builder β installs production dependencies only
- Final image β Alpine Node.js with runtime files only
# Build and run locally
docker compose up -d --build
# Force a card data reimport (preserves user decks)
FORCE_REIMPORT=true docker compose up -d- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
- Card data: MTGJSON
- Prices & marketplace: Mana Pool
- Push notifications: ntfy
- Icons: Phosphor Icons
- Charts: Chart.js
MIT β see LICENSE file for details.
Made with love for the Magic: The Gathering community.