feat: add CoopCredits Solana media rewards ecosystem

Add complete  token system with Plex/Tautulli/Overseer integration:
- Anchor program for SPL token mint/burn/transfer
- Express backend with OAuth, webhooks, Solana integration
- Next.js frontend with dashboard, admin panel, wallet management
- Docker deployment for 172.20.1.0/24 infrastructure
- Production configs with SSL, Nginx, health monitoring

Tautulli webhooks auto-mint  on watch events.
Overseer integration burns  for content requests.
This commit is contained in:
2026-04-14 11:09:50 -04:00
parent f106328f6a
commit e8d9b1fd42
69 changed files with 11382 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
import { Router } from 'express';
import axios from 'axios';
import { authenticate, AuthenticatedRequest } from '../middleware/auth';
import { prisma } from '../utils/prisma';
import { asyncHandler } from '../middleware/errorHandler';
import { getTokenBalance } from '../services/solana';
import { io } from '../index';
const router = Router();
const OVERSEER_URL = process.env.OVERSEER_URL || '';
const OVERSEER_API_KEY = process.env.OVERSEER_API_KEY || '';
const overseerClient = axios.create({
baseURL: `${OVERSEER_URL}/api/v1`,
headers: {
'X-Api-Key': OVERSEER_API_KEY
}
});
// Get request costs
router.get('/costs',
authenticate,
asyncHandler(async (_req: AuthenticatedRequest, res) => {
const settings = await prisma.systemSettings.findFirst();
res.json({
movie: settings?.movieRequestCost || 100,
tv: settings?.tvRequestCost || 200,
tvPerSeason: settings?.tvPerSeasonCost || 50
});
})
);
// Get user's balance and request availability
router.get('/balance',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const user = await prisma.user.findUnique({
where: { id: req.user!.id }
});
if (!user?.walletAddress) {
return res.json({
hasWallet: false,
balance: 0,
canRequest: false
});
}
const balance = await getTokenBalance(user.walletAddress);
const settings = await prisma.systemSettings.findFirst();
res.json({
hasWallet: true,
balance,
canRequestMovie: balance >= (settings?.movieRequestCost || 100),
canRequestTV: balance >= (settings?.tvRequestCost || 200),
costs: {
movie: settings?.movieRequestCost || 100,
tv: settings?.tvRequestCost || 200
}
});
})
);
// Search for content
router.get('/search',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { query } = req.query;
if (!query) {
return res.status(400).json({ error: 'Query required' });
}
const response = await overseerClient.get('/search', {
params: { query }
});
res.json(response.data);
})
);
// Request content
router.post('/request',
authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => {
const { mediaType, mediaId, title, seasons } = req.body;
if (!mediaType || !mediaId || !title) {
return res.status(400).json({ error: 'Missing required fields' });
}
const user = await prisma.user.findUnique({
where: { id: req.user!.id }
});
if (!user?.walletAddress) {
return res.status(400).json({ error: 'Wallet required' });
}
const settings = await prisma.systemSettings.findFirst();
// Calculate cost
let cost = 0;
if (mediaType === 'movie') {
cost = settings?.movieRequestCost || 100;
} else if (mediaType === 'tv') {
cost = settings?.tvRequestCost || 200;
if (seasons && seasons.length > 1) {
cost += (seasons.length - 1) * (settings?.tvPerSeasonCost || 50);
}
}
// Check balance
const balance = await getTokenBalance(user.walletAddress);
if (balance < cost) {
return res.status(400).json({
error: 'Insufficient balance',
required: cost,
current: balance
});
}
// Create request in Overseer
const overseerRequest = await overseerClient.post('/request', {
mediaType,
mediaId,
...(seasons && { seasons })
});
// Create local request record
const request = await prisma.contentRequest.create({
data: {
userId: user.id,
overseerRequestId: overseerRequest.data.id,
mediaType,
tmdbId: mediaId,
title,
creditsCost: cost,
status: 'PENDING',
requestedAt: new Date()
}
});
res.json({
success: true,
request,
cost,
message: 'Request submitted. Credits will be deducted when approved.'
});
})
);
// Webhook: Handle Overseer request status changes
router.post('/webhook',
asyncHandler(async (req, res) => {
const { request_id, status } = req.body;
if (!request_id || !status) {
return res.status(400).json({ error: 'Missing fields' });
}
// Find local request
const request = await prisma.contentRequest.findFirst({
where: { overseerRequestId: request_id },
include: { user: true }
});
if (!request) {
return res.status(404).json({ error: 'Request not found' });
}
// Update status
const updatedRequest = await prisma.contentRequest.update({
where: { id: request.id },
data: { status }
});
// If approved, deduct credits
if (status === 'APPROVED' && !request.user.totalSpent) {
// Note: Actual burning would happen here
// For now, we just record the transaction
const transaction = await prisma.transaction.create({
data: {
userId: request.userId,
type: 'SPEND',
amount: request.creditsCost,
requestId: request.id,
description: `Request: ${request.title}`,
contentTitle: request.title
}
});
// Update user stats
await prisma.user.update({
where: { id: request.userId },
data: {
totalSpent: { increment: request.creditsCost }
}
});
// Emit update
io.to(`user:${request.userId}`).emit('credits_spent', {
amount: request.creditsCost,
title: request.title,
transaction
});
}
res.json({ success: true, request: updatedRequest });
})
);
export { router as overseerRouter };