Snakey uses Supabase (PostgreSQL) as the primary database for all data:
| Data Type | Storage | Purpose |
|---|---|---|
| Games, Results | Supabase | Game sessions, provably fair data |
| Players, Leaderboard | Supabase | Stats, rankings, wallet addresses |
| Jackpot Pool, Tickets, Wins | Supabase | Lottery state and history |
| Payments (x402) | Supabase | Financial audit trail |
| Failed Payments | Supabase | Payment recovery (ISSUE-001) |
| Payouts | Supabase | Prize distribution tracking |
| Agents (ERC-8004) | Supabase | Agent identity and reputation |
Legacy fallback: JSON file storage (src/storage.js) remains for development/testing but is not used in production.
Snakey uses Supabase as its database provider. Supabase is a managed PostgreSQL service - meaning our database IS PostgreSQL, just hosted and managed by Supabase with extra features like:
- Real-time subscriptions
- Auto-generated REST API
- Row Level Security (RLS)
- Edge Functions (serverless)
- Built-in auth (not used - agents use wallets)
TL;DR: Supabase = PostgreSQL + extras. When you query Supabase, you're querying PostgreSQL.
| Property | Value |
|---|---|
| Project Name | snakey-agents |
| Project ID | YOUR_PROJECT_ID |
| Region | us-west-2 |
| PostgreSQL Version | 17.6 |
| Database URL | https://YOUR_PROJECT_ID.supabase.co |
| Dashboard | Supabase Dashboard |
const { createClient } = require('@supabase/supabase-js');
// Public client - respects RLS policies
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
// Admin client - bypasses RLS (server-side only!)
const supabaseAdmin = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);For tools that need raw PostgreSQL (like psql, pgAdmin, Drizzle ORM):
Host: db.YOUR_PROJECT_ID.supabase.co
Port: 5432
Database: postgres
User: postgres
Password: [DB_PASSWORD from .env]
SSL: Required
The Supabase MCP plugin provides direct database access via Claude Code:
mcp__plugin_supabase_supabase__execute_sql
mcp__plugin_supabase_supabase__apply_migration
mcp__plugin_supabase_supabase__list_tables
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ agents │────▶│ players │────▶│ games │
│ (ERC-8004) │ │(leaderboard)│ │ (sessions) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ jackpot_ │ │ payouts │
│ tickets │ │ │
└─────────────┘ └─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐
│ jackpot_ │◀────│ jackpot_ │
│ pool │ │ wins │
└─────────────┘ └─────────────┘
┌─────────────┐
│ payments │ (x402 payment records)
│ │
└─────────────┘
Stores each game round with provably fair cryptographic data.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
game_id |
TEXT | Unique game identifier (e.g., game-1234567890) |
status |
TEXT | pending, active, completed |
entry_fee |
DECIMAL | Entry fee (default: 3.00 USDC) |
prize_pool |
DECIMAL | Total prize pool for game |
jackpot_contribution |
DECIMAL | Amount added to jackpot (40% of entries) |
players |
JSONB | Array of player objects |
results |
JSONB | Final game results |
winner_wallet |
TEXT | Winner's wallet address |
server_seed |
TEXT | Server's random seed |
client_seed |
TEXT | Combined client seeds |
combined_hash |
TEXT | SHA256 hash for verification |
started_at |
TIMESTAMPTZ | Game start time |
completed_at |
TIMESTAMPTZ | Game end time |
created_at |
TIMESTAMPTZ | Record creation time |
Aggregated player statistics across all games.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
wallet_address |
TEXT | Unique wallet address |
display_name |
TEXT | Agent display name |
agent_id |
TEXT | ERC-8004 agent identity |
games_played |
INTEGER | Total games played |
wins |
INTEGER | Total wins |
total_rounds |
INTEGER | Total rounds survived |
total_battles |
INTEGER | Total battles won |
total_score |
INTEGER | Cumulative score |
total_winnings |
DECIMAL | Total USDC won |
last_played |
TIMESTAMPTZ | Last game timestamp |
created_at |
TIMESTAMPTZ | First seen timestamp |
Single-row table tracking the current jackpot pool.
| Column | Type | Description |
|---|---|---|
id |
INTEGER | Always 1 (singleton) |
current_amount |
DECIMAL | Current pool balance |
total_contributed |
DECIMAL | All-time contributions |
games_count |
INTEGER | Games that contributed |
last_mini_win |
TIMESTAMPTZ | Last MINI jackpot |
last_mega_win |
TIMESTAMPTZ | Last MEGA jackpot |
last_ultra_win |
TIMESTAMPTZ | Last ULTRA jackpot |
updated_at |
TIMESTAMPTZ | Last update time |
Tracks how many jackpot tickets each wallet holds.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
wallet_address |
TEXT | Unique wallet |
tickets |
INTEGER | Current ticket count |
created_at |
TIMESTAMPTZ | First ticket time |
updated_at |
TIMESTAMPTZ | Last ticket added |
Ticket Logic:
- 1 game played = 1 ticket
- More tickets = higher chance of being selected as winner
- ULTRA jackpot resets all tickets to 0
Records every jackpot win for transparency.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
tier |
TEXT | mini, mega, ultra |
amount |
DECIMAL | Total amount distributed |
pool_before |
DECIMAL | Pool before win |
pool_after |
DECIMAL | Pool after win |
winners |
JSONB | Array of {wallet, share, amount} |
tx_signature |
TEXT | Blockchain transaction |
created_at |
TIMESTAMPTZ | Win timestamp |
Tracks all prize payouts (game prizes and jackpots).
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
game_id |
TEXT | Associated game |
wallet_address |
TEXT | Recipient wallet |
amount |
DECIMAL | Payout amount |
place |
INTEGER | Placement (1st, 2nd, etc.) |
payout_type |
TEXT | prize or jackpot |
status |
TEXT | pending, processing, completed, failed |
tx_signature |
TEXT | Blockchain transaction |
error_message |
TEXT | Error if failed |
created_at |
TIMESTAMPTZ | Created time |
completed_at |
TIMESTAMPTZ | Completed time |
Records incoming payments via x402 protocol.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
payment_id |
TEXT | Unique payment ID |
game_id |
TEXT | Associated game (if any) |
wallet_address |
TEXT | Payer wallet |
amount |
DECIMAL | Payment amount |
currency |
TEXT | USDC (default) |
network |
TEXT | base (default) |
status |
TEXT | pending, confirmed, failed |
x402_payload |
JSONB | Full x402 payment data |
tx_hash |
TEXT | Blockchain transaction |
created_at |
TIMESTAMPTZ | Payment initiated |
confirmed_at |
TIMESTAMPTZ | Payment confirmed |
Stores payments that failed to record, enabling recovery and audit.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
payment_id |
TEXT | Original payment ID |
wallet_address |
TEXT | Payer wallet |
amount |
DECIMAL | Payment amount |
x402_payload |
JSONB | Full x402 payment data |
error_message |
TEXT | Why recording failed |
recovered |
BOOLEAN | Whether recovered |
created_at |
TIMESTAMPTZ | When failure occurred |
Purpose: When a payment is verified but fails to record to the main payments table, it's stored here to prevent lost payments. Admin can manually recover.
Stores registered AI agent identities.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
agent_id |
TEXT | Unique ERC-8004 ID |
wallet_address |
TEXT | Agent's wallet |
reputation_score |
INTEGER | Trust score |
total_games |
INTEGER | Games played |
is_validated |
BOOLEAN | Identity verified |
metadata |
JSONB | Additional agent data |
created_at |
TIMESTAMPTZ | Registration time |
updated_at |
TIMESTAMPTZ | Last update |
All tables have RLS enabled for security:
These tables allow anyone to read (for transparency):
games- Anyone can verify game resultsplayers- Leaderboard is publicjackpot_pool- Current jackpot is publicjackpot_wins- Win history is public
These tables require the service role key (server-side only):
- All write operations
payouts- Sensitive financial datajackpot_tickets- Ticket countsagents- Agent managementpayments- Payment records
-- Example policy
CREATE POLICY "Public read games" ON games FOR SELECT USING (true);
CREATE POLICY "Service write games" ON games FOR ALL USING (auth.role() = 'service_role');Agent pays $0.25 USDC via x402
│
▼
┌─────────────────────┐
│ Record in payments │
│ status: 'pending' │
└─────────────────────┘
│
▼ (x402 confirmation)
┌─────────────────────┐
│ Update payments │
│ status: 'confirmed' │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Add to games.players│
│ Add jackpot ticket │
└─────────────────────┘
Game ends (≤10 players remain)
│
▼
┌─────────────────────┐
│ Update games │
│ status: 'completed' │
│ results: [...] │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Create payouts │
│ 1st: 50% pool │
│ 2nd: 30% pool │
│ 3rd: 15% pool │
│ 4th+: 5% split │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Update jackpot_pool │
│ +40% of entry fees │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Roll jackpot │
│ (10%/1%/0.1% odds) │
└─────────────────────┘
Random roll succeeds
│
▼
┌─────────────────────┐
│ Select winners from │
│ jackpot_tickets │
│ (weighted random) │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Record jackpot_wins │
│ Create payouts │
│ Update jackpot_pool │
└─────────────────────┘
│
▼ (if ULTRA)
┌─────────────────────┐
│ Reset all tickets │
│ to 0 │
└─────────────────────┘
const db = require('./src/lib/supabase');
// Create a game
const game = await db.createGame({
game_id: 'game-123',
entry_fee: 3.00,
players: JSON.stringify([...])
});
// Get leaderboard
const leaders = await db.getLeaderboard(10);
// Add to jackpot
await db.addToJackpotPool(0.10); // 40% of $0.25
// Add tickets
await db.addJackpotTickets('0xWallet...', 1);-- Run via mcp__plugin_supabase_supabase__execute_sql
-- Get current jackpot
SELECT * FROM jackpot_pool;
-- Get top players
SELECT wallet_address, wins, total_winnings
FROM players
ORDER BY wins DESC
LIMIT 10;
-- Get recent games
SELECT game_id, status, prize_pool, created_at
FROM games
ORDER BY created_at DESC
LIMIT 5;psql "postgresql://postgres:[PASSWORD]@db.YOUR_PROJECT_ID.supabase.co:5432/postgres"Run the connection test:
node tests/test-supabase.jsExpected output:
✅ All tests passed! Database connection working.
Add these to your .env:
# Supabase (Database)
SUPABASE_URL=https://YOUR_PROJECT_ID.supabase.co
SUPABASE_ANON_KEY=eyJhbGci... # Public key (safe for frontend)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci... # Secret key (server only!)Security Notes:
SUPABASE_ANON_KEY- Can be exposed to frontend, RLS protects dataSUPABASE_SERVICE_ROLE_KEY- NEVER expose to frontend, bypasses all security
Migrations are tracked in Supabase:
| Version | Name | Description |
|---|---|---|
| 20260203061203 | initial_schema |
Create all 8 tables with indexes |
| 20260203061213 | enable_rls_policies |
Enable RLS and create policies |
| 20260203_001 | create_increment_tickets_function |
FIX 3: Atomic jackpot ticket increment RPC |
| 20260203_002 | add_payout_retry_columns |
FIX 10: Add retry_count, error_message, failed_at to payouts |
| 20260203_003 | create_complete_game_atomic_function |
FIX 7: Transaction wrapper for atomic game completion |
| 20260204_001 | create_failed_payments_table |
ISSUE-001: Payment recovery table |
To add a new migration:
// Via MCP
mcp__plugin_supabase_supabase__apply_migration({
project_id: 'YOUR_PROJECT_ID',
name: 'add_new_feature',
query: 'ALTER TABLE games ADD COLUMN new_column TEXT;'
});snakey/
├── .env # Database credentials
├── src/
│ ├── lib/
│ │ └── supabase.js # Supabase client & helpers
│ └── types/
│ └── database.ts # TypeScript types (auto-generated)
├── tests/
│ └── test-supabase.js # Connection test
└── docs/
└── DATABASE.md # This file
Check your .env file exists and has the correct values.
You're using the anon key for a write operation. Use supabaseAdmin instead.
You're trying to insert a record that already exists. Use upsert instead.
Check if the Supabase project is paused (free tier pauses after inactivity).