feat: "skip" hopium on low confidence, refund

This commit is contained in:
Face 2025-06-14 19:33:12 +06:00
parent 1ab442045f
commit 214c7cf3df
8 changed files with 1841 additions and 6 deletions

View file

@ -0,0 +1 @@
CREATE INDEX IF NOT EXISTS "prediction_question_status_resolution_idx" ON "prediction_question" USING btree ("status","resolution_date");

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,13 @@
"when": 1749654046953, "when": 1749654046953,
"tag": "0000_crazy_bloodstrike", "tag": "0000_crazy_bloodstrike",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1749907594739,
"tag": "0001_cuddly_dormammu",
"breakpoints": true
} }
] ]
} }

View file

@ -190,6 +190,7 @@ export const predictionQuestion = pgTable("prediction_question", {
creatorIdIdx: index("prediction_question_creator_id_idx").on(table.creatorId), creatorIdIdx: index("prediction_question_creator_id_idx").on(table.creatorId),
statusIdx: index("prediction_question_status_idx").on(table.status), statusIdx: index("prediction_question_status_idx").on(table.status),
resolutionDateIdx: index("prediction_question_resolution_date_idx").on(table.resolutionDate), resolutionDateIdx: index("prediction_question_resolution_date_idx").on(table.resolutionDate),
statusResolutionIdx: index("prediction_question_status_resolution_idx").on(table.status, table.resolutionDate),
}; };
}); });

View file

@ -38,7 +38,91 @@ export async function resolveExpiredQuestions() {
); );
if (resolution.confidence < 50) { if (resolution.confidence < 50) {
console.log(`Skipping question ${question.id} due to low confidence: ${resolution.confidence}`); console.log(`Cancelling question ${question.id} due to low confidence: ${resolution.confidence}`);
await db.transaction(async (tx) => {
// Mark question as cancelled
await tx
.update(predictionQuestion)
.set({
status: 'CANCELLED',
resolvedAt: now,
})
.where(eq(predictionQuestion.id, question.id));
// Get all bets for this question
const bets = await tx
.select({
id: predictionBet.id,
userId: predictionBet.userId,
side: predictionBet.side,
amount: predictionBet.amount,
})
.from(predictionBet)
.where(and(
eq(predictionBet.questionId, question.id),
isNull(predictionBet.settledAt)
));
const notificationsToCreate: Array<{
userId: number;
amount: number;
}> = [];
// Refund all bets
for (const bet of bets) {
const refundAmount = Number(bet.amount);
// Mark bet as settled with full refund
await tx
.update(predictionBet)
.set({
actualWinnings: refundAmount.toFixed(8),
settledAt: now,
})
.where(eq(predictionBet.id, bet.id));
// Refund the user
if (bet.userId !== null) {
const [userData] = await tx
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
.from(user)
.where(eq(user.id, bet.userId))
.limit(1);
if (userData) {
const newBalance = Number(userData.baseCurrencyBalance) + refundAmount;
await tx
.update(user)
.set({
baseCurrencyBalance: newBalance.toFixed(8),
updatedAt: now,
})
.where(eq(user.id, bet.userId));
}
notificationsToCreate.push({
userId: bet.userId,
amount: refundAmount
});
}
}
// Create refund notifications for all users who had bets
for (const notifData of notificationsToCreate) {
const { userId, amount } = notifData;
const title = 'Prediction skipped 🥀';
const message = `You received a full refund of ${formatValue(amount)} for "${question.question}". We recommend betting on more reliable predictions!`;
await createNotification(
userId.toString(),
'HOPIUM',
title,
message,
);
}
});
continue; continue;
} }
@ -139,7 +223,6 @@ export async function resolveExpiredQuestions() {
} }
}); });
console.log(`Successfully resolved question ${question.id}: ${resolution.resolution ? 'YES' : 'NO'} (confidence: ${resolution.confidence}%)`);
} catch (error) { } catch (error) {
console.error(`Failed to resolve question ${question.id}:`, error); console.error(`Failed to resolve question ${question.id}:`, error);
} }

View file

@ -2,7 +2,7 @@ import { auth } from '$lib/auth';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { predictionQuestion, user, predictionBet } from '$lib/server/db/schema'; import { predictionQuestion, user, predictionBet } from '$lib/server/db/schema';
import { eq, desc, and, sum, count } from 'drizzle-orm'; import { eq, desc, and, sum, count, or } from 'drizzle-orm';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, request }) => { export const GET: RequestHandler = async ({ url, request }) => {
@ -24,9 +24,22 @@ export const GET: RequestHandler = async ({ url, request }) => {
const userId = session?.user ? Number(session.user.id) : null; const userId = session?.user ? Number(session.user.id) : null;
try { try {
let statusFilter;
if (status === 'ACTIVE') {
statusFilter = eq(predictionQuestion.status, 'ACTIVE');
} else if (status === 'RESOLVED') {
statusFilter = or(
eq(predictionQuestion.status, 'RESOLVED'),
eq(predictionQuestion.status, 'CANCELLED')
);
} else {
statusFilter = undefined;
}
const conditions = []; const conditions = [];
if (status !== 'ALL') { if (statusFilter) {
conditions.push(eq(predictionQuestion.status, status as any)); conditions.push(statusFilter);
} }
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined; const whereCondition = conditions.length > 0 ? and(...conditions) : undefined;

View file

@ -174,7 +174,7 @@
<div class="text-center"> <div class="text-center">
<h1 class="mb-2 flex items-center justify-center gap-2 text-3xl font-bold"> <h1 class="mb-2 flex items-center justify-center gap-2 text-3xl font-bold">
<Sparkles class="h-8 w-8 text-purple-500" /> <Sparkles class="h-8 w-8 text-purple-500" />
Hopium<span class="text-xs">[BETA]</span> Hopium
</h1> </h1>
<p class="text-muted-foreground mb-6"> <p class="text-muted-foreground mb-6">
AI-powered prediction markets. Create questions and bet on outcomes. AI-powered prediction markets. Create questions and bet on outcomes.
@ -236,6 +236,11 @@
NO NO
{/if} {/if}
</Badge> </Badge>
{:else if question.status === 'CANCELLED'}
<Badge variant="outline" class="flex flex-shrink-0 items-center gap-1 text-muted-foreground border-muted-foreground">
<XIcon class="h-3 w-3" />
SKIP
</Badge>
{/if} {/if}
<!-- Probability Meter --> <!-- Probability Meter -->

View file

@ -242,6 +242,11 @@
RESOLVED: NO RESOLVED: NO
{/if} {/if}
</Badge> </Badge>
{:else if question.status === 'CANCELLED'}
<Badge variant="outline" class="text-muted-foreground border-muted-foreground">
<XIcon class="h-4 w-4" />
SKIP
</Badge>
{/if} {/if}
</div> </div>
</div> </div>