docs: Zaps.md — Lightning payment integration plan
Comprehensive implementation guide for 21-sat ranked fights (winner takes all). Covers NWC + Cashu payment flows, database schema, server/frontend changes, edge cases, security, and phased implementation order. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e31b15ed28
commit
88d0495910
@@ -0,0 +1,521 @@
|
||||
# Zaps.md — Lightning Payment Integration Plan
|
||||
|
||||
> Implementation guide for adding 21-sat ranked fights to botfights.
|
||||
> Winner takes all. No custodial wallets. Bitcoin only.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Ranked fights cost 21 sats per player (42 sats pot). Winner takes all. The server holds funds for **seconds** (fight duration only) — there are no user balances, no deposits, no withdrawal pages. Free fights remain unchanged.
|
||||
|
||||
### Philosophy
|
||||
- **Non-custodial**: Server operates a transient escrow wallet, not a custody service
|
||||
- **Privacy-first**: NWC protocol via nostr-tools directly — no third-party SDKs phoning home
|
||||
- **Open source only**: `nostr-tools` (MIT) + `@cashu/cashu-ts` (MIT)
|
||||
- **Bitcoin only**: Lightning for payments, Cashu ecash as alternative
|
||||
|
||||
---
|
||||
|
||||
## Payment Architecture
|
||||
|
||||
```
|
||||
ENTRY (before fight starts):
|
||||
Player A ──21 sats──▶ Lightning Invoice ──▶ Server Wallet
|
||||
Player B ──21 sats──▶ Lightning Invoice ──▶ Server Wallet
|
||||
Both confirmed? ──▶ Fight begins
|
||||
|
||||
PAYOUT (after fight ends):
|
||||
Server Wallet ──42 sats──▶ Winner's wallet (via NWC / Lightning Address / Cashu token)
|
||||
|
||||
REFUND (if no match found within 60s):
|
||||
Server Wallet ──21 sats──▶ Original payer (via same channel)
|
||||
```
|
||||
|
||||
### Payment Methods (user chooses one)
|
||||
|
||||
| Method | How it works | UX |
|
||||
|--------|-------------|-----|
|
||||
| **NWC (primary)** | User connects Lightning wallet once via NWC connection string. Server sends encrypted payment requests via Nostr relay. Wallet auto-approves small amounts. | Connect once, 1-click per fight |
|
||||
| **Cashu ecash** | User pastes ecash token (21 sats) from any Cashu wallet. Server redeems with mint. | Paste token per fight |
|
||||
| **Lightning Address** | For payouts only — user provides `user@domain.com` style address. Server resolves to invoice and pays. | Set once on profile |
|
||||
|
||||
### Server Wallet
|
||||
|
||||
The server needs its own small Lightning wallet for the escrow float. Configured via env var:
|
||||
|
||||
```bash
|
||||
BOTFIGHTS_NWC_URL=nostr+walletconnect://pubkey?relay=wss://relay.example.com&secret=hex
|
||||
BOTFIGHTS_CASHU_MINT_URL=https://mint.example.com # optional: for ecash fallback payouts
|
||||
```
|
||||
|
||||
Options (pick one):
|
||||
- **Alby Hub** — self-hosted Lightning node + NWC server. Simplest setup.
|
||||
- **LNbits** — self-hosted, supports NWC, can run over Tor. Best for privacy.
|
||||
- **CLN/LND** — direct node. Maximum control, most setup.
|
||||
|
||||
The wallet only ever holds transient amounts (max 42 sats per active fight). Every sat that enters is either paid out to a winner or refunded.
|
||||
|
||||
---
|
||||
|
||||
## Two Modes
|
||||
|
||||
| Aspect | Free Mode (existing) | Ranked Mode (new) |
|
||||
|--------|---------------------|-------------------|
|
||||
| Entry fee | 0 sats | 21 sats per player |
|
||||
| Prize | ELO changes only | 42 sats + ELO changes |
|
||||
| Queue endpoint | `POST /api/queue/join/:botId` | `POST /api/queue/join-ranked/:botId` |
|
||||
| Mock fallback | Yes (after timeout) | **Never** — refund on timeout |
|
||||
| Timeout | 3s dev / 30s prod | 60s, then refund |
|
||||
|
||||
Free mode is **completely unchanged**. All existing code paths remain as-is.
|
||||
|
||||
---
|
||||
|
||||
## Database Changes
|
||||
|
||||
### New table: `payments`
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id TEXT PRIMARY KEY,
|
||||
fight_id TEXT REFERENCES fights(id),
|
||||
bot_id TEXT NOT NULL REFERENCES bots(id),
|
||||
direction TEXT NOT NULL, -- 'in' (entry fee) or 'out' (prize payout)
|
||||
amount_sats INTEGER NOT NULL, -- 21 or 42
|
||||
method TEXT NOT NULL, -- 'lightning' or 'cashu'
|
||||
status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'confirmed' | 'failed' | 'refunded'
|
||||
invoice TEXT, -- bolt11 invoice string
|
||||
preimage TEXT, -- payment preimage (proof of payment)
|
||||
cashu_token TEXT, -- serialized cashu token
|
||||
error_reason TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
confirmed_at TEXT,
|
||||
refunded_at TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### New table: `wallet_connections`
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS wallet_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
bot_id TEXT NOT NULL UNIQUE REFERENCES bots(id),
|
||||
method TEXT NOT NULL, -- 'nwc' | 'lnaddress' | 'cashu_mint'
|
||||
connection_data TEXT NOT NULL, -- encrypted (AES-256-GCM) NWC string, LN address, or mint URL
|
||||
created_at TEXT NOT NULL,
|
||||
last_used_at TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### New columns on `fights`
|
||||
|
||||
```sql
|
||||
ALTER TABLE fights ADD COLUMN mode TEXT NOT NULL DEFAULT 'free'; -- 'free' | 'ranked'
|
||||
ALTER TABLE fights ADD COLUMN pot_sats INTEGER NOT NULL DEFAULT 0; -- 0 or 42
|
||||
ALTER TABLE fights ADD COLUMN payout_status TEXT; -- null | 'pending' | 'paid' | 'failed'
|
||||
```
|
||||
|
||||
### New columns on `bots`
|
||||
|
||||
```sql
|
||||
ALTER TABLE bots ADD COLUMN sats_won INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE bots ADD COLUMN sats_wagered INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE bots ADD COLUMN has_wallet INTEGER NOT NULL DEFAULT 0; -- boolean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Implementation
|
||||
|
||||
### New file: `server/src/engine/payments.ts`
|
||||
|
||||
Core payment engine. All Lightning/Cashu operations.
|
||||
|
||||
```typescript
|
||||
// NWC protocol — implemented directly via nostr-tools, no Alby SDK
|
||||
// Uses encrypted Nostr events (kind 23194 request, kind 23195 response)
|
||||
|
||||
export async function createEntryInvoice(botId: string): Promise<{ bolt11: string; paymentId: string }>
|
||||
// Generate 21-sat Lightning invoice via server's NWC wallet
|
||||
// Create payments row with status='pending'
|
||||
// Invoice expires in 10 minutes
|
||||
|
||||
export async function checkPaymentStatus(paymentId: string): Promise<'pending' | 'confirmed' | 'failed'>
|
||||
// Check if invoice has been paid (poll NWC wallet)
|
||||
// Update DB row when confirmed
|
||||
|
||||
export async function payWinner(fightId: string, winnerId: string): Promise<void>
|
||||
// Look up winner's wallet_connection
|
||||
// If NWC: send 42 sats via pay_invoice
|
||||
// If Lightning Address: resolve LNURL, create invoice, pay
|
||||
// If no wallet: create Cashu token, store in payments table for later claim
|
||||
// Retry 3x with exponential backoff
|
||||
|
||||
export async function refundEntry(paymentId: string): Promise<void>
|
||||
// Refund 21 sats back to the payer
|
||||
// Used when: no match found (timeout), fight cancelled, draw
|
||||
|
||||
export async function redeemCashuToken(token: string, botId: string): Promise<boolean>
|
||||
// Validate ecash token with mint
|
||||
// If valid: mark payment as confirmed
|
||||
// If spent/invalid: return false
|
||||
|
||||
export async function recoverOrphanedPayments(): Promise<void>
|
||||
// Called on server startup
|
||||
// Find confirmed payments without a fight_id → refund
|
||||
// Find fights with payout_status='pending' → retry payout
|
||||
```
|
||||
|
||||
### New file: `server/src/engine/ranked-queue.ts`
|
||||
|
||||
Mirrors `server/src/engine/queue.ts` structure but with payment gates.
|
||||
|
||||
```typescript
|
||||
interface RankedQueueEntry {
|
||||
botId: string
|
||||
botName: string
|
||||
webhookUrl: string
|
||||
eloRating: number
|
||||
joinedAt: number
|
||||
paymentId: string // must be confirmed before entry
|
||||
resolve: (fightId: string) => void
|
||||
reject: (error: Error) => void
|
||||
timeoutHandle: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
export function joinRankedQueue(botId: string, paymentId: string): Promise<string>
|
||||
// Verify payment is confirmed
|
||||
// Add to ranked queue
|
||||
// If another player waiting: instant ELO-based match → start ranked fight
|
||||
// If alone: wait up to 60s
|
||||
// On timeout: refund entry fee, reject with "no match found"
|
||||
// NEVER match against mock bots
|
||||
|
||||
export function getRankedQueueStatus(): { waiting: number }
|
||||
```
|
||||
|
||||
### New file: `server/src/routes/payments.ts`
|
||||
|
||||
Hono router mounted at `/api/payments`.
|
||||
|
||||
```typescript
|
||||
POST /api/payments/connect-wallet
|
||||
// Body: { pubkey, method: 'nwc' | 'lnaddress', connectionData: string }
|
||||
// Encrypts and stores wallet connection
|
||||
// Returns: { success: true }
|
||||
|
||||
GET /api/payments/wallet-status
|
||||
// Query: ?pubkey=...
|
||||
// Returns: { connected: boolean, method: string | null }
|
||||
|
||||
POST /api/payments/create-invoice
|
||||
// Body: { botId }
|
||||
// Generates 21-sat invoice
|
||||
// Returns: { bolt11: string, paymentId: string }
|
||||
|
||||
GET /api/payments/check/:paymentId
|
||||
// Poll payment status
|
||||
// Returns: { status: 'pending' | 'confirmed' | 'failed' }
|
||||
|
||||
POST /api/payments/submit-cashu
|
||||
// Body: { botId, token: string }
|
||||
// Redeems Cashu token as entry fee
|
||||
// Returns: { paymentId: string, status: 'confirmed' | 'failed' }
|
||||
|
||||
GET /api/payments/winnings/:botId
|
||||
// Get unclaimed Cashu payouts
|
||||
// Returns: { unclaimed: { paymentId, cashuToken, amountSats }[] }
|
||||
|
||||
POST /api/payments/claim/:paymentId
|
||||
// Mark Cashu payout as claimed
|
||||
// Returns: { cashuToken: string }
|
||||
|
||||
DELETE /api/payments/disconnect-wallet
|
||||
// Body: { pubkey }
|
||||
// Remove wallet connection
|
||||
```
|
||||
|
||||
### Modify: `server/src/routes/queue.ts`
|
||||
|
||||
Add ranked queue endpoint (existing free endpoint unchanged):
|
||||
|
||||
```typescript
|
||||
queueRouter.post('/join-ranked/:botId', async (c) => {
|
||||
const botId = c.req.param('botId')
|
||||
const { paymentId } = await c.req.json()
|
||||
// Verify payment is confirmed
|
||||
// Join ranked queue
|
||||
// Return fightId when matched (or error + refund on timeout)
|
||||
})
|
||||
```
|
||||
|
||||
### Modify: `server/src/engine/orchestrator.ts`
|
||||
|
||||
After the `finalize()` transaction that updates ELO:
|
||||
|
||||
```typescript
|
||||
// If ranked fight, pay the winner
|
||||
if (fight.mode === 'ranked' && winnerId) {
|
||||
payWinner(fightId, winnerId).catch(err => {
|
||||
console.error(`[payments] payout failed for fight ${fightId}:`, err)
|
||||
// payout_status stays 'pending' — recoverOrphanedPayments will retry
|
||||
})
|
||||
}
|
||||
|
||||
// If ranked fight is a draw, refund both
|
||||
if (fight.mode === 'ranked' && !winnerId) {
|
||||
// refund both entry fees
|
||||
}
|
||||
```
|
||||
|
||||
Also update `createFightRecord` to accept `mode` parameter and store `pot_sats`.
|
||||
|
||||
Update `fight_end` event to include payment info:
|
||||
```typescript
|
||||
emit(fightId, 'fight_end', {
|
||||
winnerId, winnerName, isPerfect,
|
||||
finalHp: { a: hpA, b: hpB },
|
||||
mode: fight.mode,
|
||||
potSats: fight.pot_sats,
|
||||
})
|
||||
```
|
||||
|
||||
### Modify: `server/src/db/schema.ts`
|
||||
|
||||
Add Drizzle schema for `payments` and `wallet_connections` tables. Add new columns to `fights` and `bots`.
|
||||
|
||||
### Modify: `server/src/app.ts`
|
||||
|
||||
Mount payments router:
|
||||
```typescript
|
||||
import { paymentsRouter } from './routes/payments.js'
|
||||
app.route('/api/payments', paymentsRouter)
|
||||
```
|
||||
|
||||
Call `recoverOrphanedPayments()` on startup (alongside existing `cleanupOrphanedFights()`).
|
||||
|
||||
---
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### New file: `frontend/src/composables/useWallet.ts`
|
||||
|
||||
Follows same pattern as `frontend/src/composables/useNostr.ts`.
|
||||
|
||||
```typescript
|
||||
export function useWallet() {
|
||||
const isWalletConnected = ref(false)
|
||||
const walletMethod = ref<'nwc' | 'lnaddress' | 'cashu' | null>(null)
|
||||
const paymentStatus = ref<'idle' | 'invoiced' | 'paying' | 'confirmed' | 'failed'>('idle')
|
||||
const pendingPayment = ref<{ paymentId: string; bolt11: string } | null>(null)
|
||||
|
||||
async function connectNWC(connectionString: string): Promise<void>
|
||||
// POST /api/payments/connect-wallet
|
||||
// Persist to localStorage (bf_wallet_method)
|
||||
|
||||
async function connectLightningAddress(address: string): Promise<void>
|
||||
// POST /api/payments/connect-wallet
|
||||
|
||||
async function disconnectWallet(): Promise<void>
|
||||
// DELETE /api/payments/disconnect-wallet
|
||||
|
||||
async function checkWalletStatus(): Promise<void>
|
||||
// GET /api/payments/wallet-status
|
||||
|
||||
async function payEntryFee(botId: string): Promise<string>
|
||||
// 1. POST /api/payments/create-invoice → { bolt11, paymentId }
|
||||
// 2. If NWC connected: send pay_invoice via NWC protocol (WebSocket to relay)
|
||||
// 3. If no NWC: return bolt11 for QR display
|
||||
// 4. Poll GET /api/payments/check/:paymentId until confirmed
|
||||
// Returns paymentId
|
||||
|
||||
async function submitCashuToken(botId: string, token: string): Promise<string>
|
||||
// POST /api/payments/submit-cashu
|
||||
// Returns paymentId
|
||||
|
||||
return { isWalletConnected, walletMethod, paymentStatus, pendingPayment,
|
||||
connectNWC, connectLightningAddress, disconnectWallet,
|
||||
checkWalletStatus, payEntryFee, submitCashuToken }
|
||||
}
|
||||
```
|
||||
|
||||
NWC client protocol (in this composable):
|
||||
1. Parse `nostr+walletconnect://` URI → extract pubkey, relay URL, secret
|
||||
2. Open WebSocket to relay
|
||||
3. Create kind 23194 event (pay_invoice request) encrypted with NIP-44
|
||||
4. Wait for kind 23195 response event
|
||||
5. Decrypt and verify preimage
|
||||
|
||||
Use `nostr-tools` for event creation, encryption, and signing. No Alby SDK.
|
||||
|
||||
### New file: `frontend/src/components/WalletConnect.vue`
|
||||
|
||||
Minimal component for wallet connection. States:
|
||||
|
||||
1. **No wallet**: "CONNECT WALLET" button → opens modal/inline form with two options:
|
||||
- Paste NWC connection string
|
||||
- Enter Lightning Address (for payouts only)
|
||||
2. **Connected**: Small lightning bolt icon + "WALLET READY" + disconnect link
|
||||
3. **Paying**: "PAYING 21 SATS..." spinner (NWC auto-pay) OR QR code (manual pay)
|
||||
4. **Confirmed**: Brief green flash "LOCKED IN" then transitions to queue
|
||||
|
||||
Follows existing design: neon colors, `font-display`, `tracking-wider`, `border-2 border-neon-*`.
|
||||
|
||||
### Modify: `frontend/src/pages/JoinBoutPage.vue`
|
||||
|
||||
In the "ready to fight" step, add ranked option below the existing FIGHT button:
|
||||
|
||||
```html
|
||||
<!-- Existing free fight (unchanged) -->
|
||||
<button @click="fight">FIGHT FREE</button>
|
||||
|
||||
<!-- New ranked fight -->
|
||||
<button @click="fightRanked" :disabled="!isWalletConnected">
|
||||
FIGHT FOR SATS
|
||||
<span class="text-[9px]">21 SATS — WINNER TAKES ALL</span>
|
||||
</button>
|
||||
|
||||
<!-- Wallet connection (shown if no wallet) -->
|
||||
<WalletConnect v-if="!isWalletConnected" />
|
||||
```
|
||||
|
||||
The `fightRanked()` flow:
|
||||
1. `payEntryFee(botId)` → creates invoice, pays via NWC or shows QR
|
||||
2. Poll until confirmed
|
||||
3. `POST /api/queue/join-ranked/:botId` with `{ paymentId }`
|
||||
4. Navigate to `/arena/:fightId` when matched
|
||||
|
||||
### Modify: `frontend/src/pages/FightPage.vue`
|
||||
|
||||
- During ranked fight: show "42 SATS ON THE LINE" badge
|
||||
- After ranked fight: winner sees "YOU WON 42 SATS!" with lightning animation
|
||||
- The `fight_end` SSE event carries `mode` and `potSats`
|
||||
|
||||
### Modify: `frontend/src/pages/BotProfilePage.vue`
|
||||
|
||||
- Show sats stats: "SATS WON: 420 | SATS WAGERED: 630"
|
||||
- Wallet connection section
|
||||
- Unclaimed Cashu payouts (if any)
|
||||
|
||||
### Modify: `frontend/src/composables/useNostr.ts`
|
||||
|
||||
Add `satsWon`, `satsWagered`, `hasWallet` to the `BotData` interface.
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
| Scenario | Handling |
|
||||
|----------|----------|
|
||||
| Payment fails during invoice creation | User sees error, can retry. No sats lost. |
|
||||
| Payment confirmed but no match (60s timeout) | Automatic refund. Payment row → `refunded`. |
|
||||
| Player A pays, Player B's payment fails | Player A is refunded. Neither enters ranked queue. |
|
||||
| Fight crashes mid-way | Fight → `cancelled`. Both entry fees refunded. |
|
||||
| Winner's wallet offline for payout | Retry 3x with backoff. If all fail → create Cashu token in DB. Winner claims on profile page. |
|
||||
| Draw in ranked fight | Both get 21 sats back (refund). |
|
||||
| Server restart with pending payments | `recoverOrphanedPayments()` on startup: refund orphaned confirmed entries, retry pending payouts. |
|
||||
| Cashu double-spend attempt | Token redeemed with mint immediately. Mint rejects = payment fails. |
|
||||
| Mock bot wins ranked fight | Impossible. Ranked queue never matches mocks. |
|
||||
| User disconnects during payment | Invoice expires in 10 minutes. Confirmed-but-unmatched payments are refunded on recovery. |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
```json
|
||||
// Server — add to server/package.json
|
||||
{
|
||||
"nostr-tools": "^2.x",
|
||||
"@cashu/cashu-ts": "^2.x"
|
||||
}
|
||||
|
||||
// Frontend — add to frontend/package.json
|
||||
{
|
||||
"nostr-tools": "^2.x"
|
||||
}
|
||||
```
|
||||
|
||||
Both MIT licensed, open source, no telemetry.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Required for ranked fights
|
||||
BOTFIGHTS_NWC_URL=nostr+walletconnect://pubkey?relay=wss://relay.example.com&secret=hex
|
||||
|
||||
# Optional: Cashu mint for ecash fallback payouts
|
||||
BOTFIGHTS_CASHU_MINT_URL=https://mint.example.com
|
||||
|
||||
# Optional: encryption key for wallet_connections table (auto-generated if missing)
|
||||
BOTFIGHTS_WALLET_ENCRYPTION_KEY=hex-encoded-32-byte-key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Database + Payment Engine (server only)
|
||||
1. Add new columns to `fights` and `bots` in `server/src/db/schema.ts`
|
||||
2. Create `payments` and `wallet_connections` tables
|
||||
3. Run migrations in `server/src/db/startup.ts`
|
||||
4. Implement `server/src/engine/payments.ts` (NWC protocol, invoice creation, status checking, payouts)
|
||||
5. Implement `server/src/routes/payments.ts` (all endpoints)
|
||||
6. Test with curl: create invoice, simulate payment, check status
|
||||
|
||||
### Phase 2: Ranked Queue (server only)
|
||||
7. Implement `server/src/engine/ranked-queue.ts`
|
||||
8. Add `POST /api/queue/join-ranked/:botId` to queue routes
|
||||
9. Modify orchestrator: accept `mode` param, trigger `payWinner()` on ranked fight end
|
||||
10. Add `recoverOrphanedPayments()` to startup
|
||||
11. Test: two bots pay and fight, winner receives 42 sats
|
||||
|
||||
### Phase 3: Frontend Wallet
|
||||
12. Implement `frontend/src/composables/useWallet.ts`
|
||||
13. Implement `frontend/src/components/WalletConnect.vue`
|
||||
14. Modify `JoinBoutPage.vue`: add ranked fight button + payment flow
|
||||
15. Modify `FightPage.vue`: show sats on the line, winner payout celebration
|
||||
16. Modify `BotProfilePage.vue`: sats stats + wallet management
|
||||
|
||||
### Phase 4: Cashu Alternative
|
||||
17. Add Cashu token submission to `payments.ts` + route
|
||||
18. Add Cashu payout fallback for offline winners
|
||||
19. Add claim UI on profile page
|
||||
|
||||
### Phase 5: Polish
|
||||
20. "RANKED" badge on fight cards in arena list
|
||||
21. Sats leaderboard column
|
||||
22. Error recovery hardening
|
||||
23. Rate limiting on payment endpoints
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
- **Server wallet secret**: env var only, never in DB or code
|
||||
- **User NWC strings**: encrypted at rest (AES-256-GCM) in `wallet_connections`
|
||||
- **No user balances**: server holds funds for seconds, not as deposits
|
||||
- **Invoice expiry**: 10 minutes max
|
||||
- **Rate limiting**: same pattern as existing `rateLimit` middleware
|
||||
- **Refund-on-failure**: default for every error path
|
||||
- **No tracking**: payment records use bot IDs only, no IPs or extra identifiers
|
||||
- **Cashu privacy**: ecash is unlinkable — mint can't connect sender to receiver
|
||||
|
||||
---
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `server/src/engine/queue.ts` | Pattern to follow for ranked-queue.ts |
|
||||
| `server/src/engine/orchestrator.ts` | Fight lifecycle — add payment gates + payout trigger |
|
||||
| `server/src/db/schema.ts` | Add new tables + columns |
|
||||
| `server/src/db/startup.ts` | Migration SQL |
|
||||
| `server/src/routes/queue.ts` | Add ranked endpoint |
|
||||
| `server/src/app.ts` | Mount payments router + startup recovery |
|
||||
| `frontend/src/composables/useNostr.ts` | Pattern to follow for useWallet.ts |
|
||||
| `frontend/src/pages/JoinBoutPage.vue` | Add ranked fight button + wallet UI |
|
||||
| `frontend/src/pages/FightPage.vue` | Show pot + winner payout |
|
||||
| `frontend/src/pages/BotProfilePage.vue` | Sats stats + wallet management |
|
||||
Reference in New Issue
Block a user