Skills Plugins MCP Prompt Model 博客 我的中心
开发编程 #typescript #ai #web

betting-app

Build full-stack sports betting web apps using pari-mutuel odds with Next.js 14 (App Router), TypeScript, Tailwind CSS, and Supabase (PostgreSQL + RLS). Encodes proven patterns for wallet management, atomic bet placement, dynamic odds calculation, admin dashboards, leaderboards, live score integration, and early-bird incentive mechanics. Use this skill whenever someone wants to build a betting platform, prediction market, sports pool, fantasy league money app, or any app where users wager on outcomes with a shared prize pool. Also trigger for requests to add betting markets, pari-mutuel odds, wallet systems, or settlement logic to an existing app.

DeepseekModel 官方收录技能 质量 良好 · 48 v1.0.0

获取

https://deepseekmodel.com/api/download.php?id=sklls-betting-app-skill-skill-md&format=skill
下载 .skill 标准格式,含 system_prompt 与 model_config,导入任意 Agent 框架即可使用
.skill 文件中 system_prompt 字段的实际内容。
name betting-app description Build full-stack sports betting web apps using pari-mutuel odds with Next.js 14 (App Router), TypeScript, Tailwind CSS, and Supabase (PostgreSQL + RLS). Encodes proven patterns for wallet management, atomic bet placement, dynamic odds calculation, admin dashboards, leaderboards, live score integration, and early-bird incentive mechanics. Use this skill whenever someone wants to build a betting platform, prediction market, sports pool, fantasy league money app, or any app where users wager on outcomes with a shared prize pool. Also trigger for requests to add betting markets, pari-mutuel odds, wallet systems, or settlement logic to an existing app. Betting App Skill You are building a full-stack sports betting platform. This skill encodes hard-won patterns from shipping a real cricket betting app (BCL Bet) that ran a complete tournament with 15+ players, 200+ bets, and zero data integrity incidents. Reference implementation: https://github.com/sklls/BPL_BET Core principle : Keep financial logic in Postgres RPC functions, not in application code. The database is the only place that can enforce atomicity. 1. Database Schema CREATE TYPE user_role AS ENUM ( 'user' , 'admin' ); CREATE TYPE market_status AS ENUM ( 'open' , 'closed' , 'settled' ); CREATE TYPE bet_status AS ENUM ( 'pending' , 'won' , 'lost' , 'void' ); CREATE TYPE transaction_type AS ENUM ( 'bet' , 'win' , 'topup' , 'refund' ); -- User wallets and roles CREATE TABLE profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id), display_name TEXT, role user_role DEFAULT 'user' , wallet_balance DECIMAL ( 12 , 2 ) DEFAULT 0 CHECK (wallet_balance >= 0 ) ); -- The event being bet on CREATE TABLE matches ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), team_a TEXT NOT NULL , team_b TEXT NOT NULL , match_date TIMESTAMPTZ, venue TEXT, status TEXT DEFAULT 'upcoming' , created_at TIMESTAMPTZ DEFAULT NOW() ); -- A betting question ("Who wins?", "Top scorer?") CREATE TABLE markets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), match_id UUID REFERENCES matches (id) ON DELETE CASCADE, title TEXT, market_type TEXT NOT NULL , status market_status DEFAULT 'open' , result TEXT, total_pool DECIMAL ( 12 , 2 ) DEFAULT 0 , house_edge_pct DECIMAL ( 5 , 2 ) DEFAULT 5.0 , created_at TIMESTAMPTZ DEFAULT NOW() ); -- The choices within a market ("Team A", "Team B") CREATE TABLE bet_options ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), market_id UUID REFERENCES markets(id) ON DELETE CASCADE, label TEXT NOT NULL , total_amount_bet DECIMAL ( 12 , 2 ) DEFAULT 0 ); -- Individual user bets CREATE TABLE bets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES profiles(id), market_id UUID REFERENCES markets(id), bet_option_id UUID REFERENCES bet_options(id), amount DECIMAL ( 12 , 2 ) NOT NULL CHECK (amount > 0 ), odds_at_placement DECIMAL ( 10 , 4 ), status bet_status DEFAULT 'pending' , payout DECIMAL ( 12 , 2 ), placed_at TIMESTAMPTZ DEFAULT NOW(), settled_at TIMESTAMPTZ ); -- Full audit trail of every wallet movement CREATE TABLE transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES profiles(id), type transaction_type NOT NULL , amount DECIMAL ( 12 , 2 ) NOT NULL , description TEXT, reference_id UUID, created_at TIMESTAMPTZ DEFAULT NOW() ); Leaderboard View CREATE VIEW leaderboard AS SELECT p.id, p.display_name, p.wallet_balance, COALESCE ( SUM ( CASE WHEN t.type = 'win' THEN t.amount ELSE 0 END ), 0 ) AS total_winnings, COUNT ( CASE WHEN b.status = 'won' THEN 1 END ) AS bets_won, COUNT (b.id) AS total_bets FROM profiles p LEFT JOIN bets b ON b.user_id = p.id LEFT JOIN transactions t ON t.user_id = p.id GROUP BY p.id, p.display_name, p.wallet_balance ORDER BY total_winnings DESC ; Row-Level Security -- Enable RLS on all tables ALTER TABLE profiles ENABLE ROW LEVEL SECURITY; ALTER TABLE bets ENABLE ROW LEVEL SECURITY; ALTER TABLE transactions ENABLE ROW LEVEL SECURITY; ALTER TABLE markets ENABLE ROW LEVEL SECURITY; ALTER TABLE bet_options ENABLE ROW LEVEL SECURITY; ALTER TABLE matches ENABLE ROW LEVEL SECURITY; -- Users see only their own financial data CREATE POLICY own_bets ON bets FOR ALL USING (auth.uid() = user_id); CREATE POLICY own_txns ON transactions FOR ALL USING (auth.uid() = user_id); -- Everyone can read public data (needed for displaying odds) CREATE POLICY pub_matches ON matches FOR SELECT USING ( true ); CREATE POLICY pub_markets ON markets FOR SELECT USING ( true ); CREATE POLICY pub_options ON bet_options FOR SELECT USING ( true ); CREATE POLICY pub_profiles ON profiles FOR SELECT USING ( true ); 2. Pari-Mutuel Odds Engine The pool is split among winners; house takes a cut. Nobody sets prices — the crowd does. Formula: odds = (total_pool / amount_on_option) x (1 - house_edge%) // lib/odds.ts export function calculateOdds ( options : { id: string ; total_amount_bet: number }[], selectedId : string , extraAmount : number = 0 , // include user's preview stake in pool houseEdgePct : number = 5 ): number { const totalPool = options. reduce ( ( s, o ) => s + o. total_amount_bet , 0 ) + extraAmount const selected = options. find ( o => o. id === selectedId) if (!selected) return 1 const amountOnSelected = selected. total_amount_bet + extraAmount if (amountOnSelected <= 0 ) return 1 const raw = totalPool / amountOnSelected return Math . max ( 1.01 , raw * ( 1 - houseEdgePct / 100 )) } export const calcPayout = ( stake : number , odds : number ) => Math . round (stake * odds * 100 ) / 100 export const formatOdds = ( odds : number ) => ` ${odds.toFixed( 2 )} x` Always pass extraAmount for preview — the player's stake is already in the pool. Clamp minimum to 1.01x so a bet is never worthless. 3. Atomic Bet Placement RPC Never do wallet deduction in application code — race conditions will allow overdrafts. CREATE OR REPLACE FUNCTION place_bet( p_user_id UUID, p_market_id UUID, p_bet_option_id UUID, p_amount DECIMAL , p_odds DECIMAL ) RETURNS JSON AS $$ DECLARE v_balance DECIMAL ; v_bet_id UUID; BEGIN -- FOR UPDATE locks the row — prevents two concurrent bets from overdrawing SELECT wallet_balance INTO v_balance FROM profiles WHERE id = p_user_id FOR UPDATE ; IF v_balance < p_amount THEN RAISE EXCEPTION 'Insufficient balance' ; END IF; UPDATE profiles SET wallet_balance = wallet_balance - p_amount WHERE id = p_user_id; INSERT INTO bets (user_id, market_id, bet_option_id, amount, odds_at_placement) VALUES (p_user_id, p_market_id, p_bet_option_id, p_amount, p_odds) RETURNING id INTO v_bet_id; UPDATE bet_options SET total_amount_bet = total_amount_bet + p_amount WHERE id = p_bet_option_id; UPDATE markets SET total_pool = total_pool + p_amount WHERE id = p_market_id; INSERT INTO transactions (user_id, type, amount, description, reference_id) VALUES (p_user_id, 'bet' , p_amount, 'Bet placed' , v_bet_id); RETURN json_build_object( 'bet_id' , v_bet_id); END ; $$ LANGUAGE plpgsql SECURITY DEFINER; In the API route, always recalculate odds server-side — never trust client-submitted odds. 4. Market Settlement (with Early Bird Bonus) CREATE OR REPLACE FUNCTION settle_market(p_market_id UUID, p_winning_option_id UUID) RETURNS VOID AS $$ DECLARE bet RECORD; v_payout DECIMAL ; v_winning_label TEXT; v_created_at TIMESTAMPTZ; v_cutoff TIMESTAMPTZ; v_is_early BOOLEAN ; BEGIN SELECT label INTO v_winning_label FROM bet_options WHERE id = p_winning_option_id; SELECT created_at INTO v_created_at FROM markets WHERE id = p_market_id; v_cutoff : = v_created_at + INTERVAL '30 minutes' ; FOR bet IN SELECT b.id, b.user_id, b.amount, b.odds_at_placement, b.placed_at FROM bets b WHERE b.market_id = p_market_id AND b.bet_option_id = p_winning_option_id AND b.status = 'pending' LOOP v_is_early : = bet.placed_at < v_cutoff; v_payout : = bet.amount * bet.odds_at_placement; IF v_is_early THEN v_payout : = v_payout * 1.10 ; END IF; -- early bird +10% UPDATE profiles SET wallet_balance = wallet_balance + v_payout WHERE id = bet.user_id; UPDATE bets SET status = 'won' , payout = v_payout, settled_at = NOW() WHERE id = bet.id; INSERT INTO transactions (user_id, type, amount, description, reference_id) VALUES ( bet.user_id, 'win' , v_payout, CASE WHEN v_is_early THEN 'Early bird +10%: ' || v_winning_label ELSE 'Bet won: ' || v_winning_label END , bet.id ); END LOOP; UPDATE bets SET status = 'lost' , payout = 0 , settled_at = NOW() WHERE market_id = p_market_id AND bet_option_id != p_winning_option_id AND status = 'pending' ; UPDATE markets SET status = 'settled' , result = v_winning_label, updated_at = NOW() WHERE id = p_market_id; END ; $$ LANGUAGE plpgsql SECURITY DEFINER; 5. Two Supabase Clients — Never Mix Them // lib/supabase-server.ts // Uses ANON key — respects RLS. For user-scoped data (own bets, own balance). export function createServerClient ( ) { /* standard Supabase SSR client */ } // Bypasses RLS entirely. SERVER-SIDE ONLY. Never expose to client. export function createAdminClient ( ) { return createClient ( process. env . NEXT_PUBLIC_SUPABASE_URL !, process. env . SUPABASE_SERVICE_ROLE_KEY !, { auth : { persistSession : false } } ) } Use admin client for: leaderboard, bettors list, admin dashboard, settlement, wallet top-up. Use server client for: user's own bets/balance, placing bets. 6. TypeScript Supabase Join Type Cast Supabase types FK joins as arrays, but FK joins to unique fields return single objects at runtime. This causes Vercel build failures if not handled. // Wrong: bet.profiles?.display_name <- TS error (thinks it's an array) // Correct: const name = (bet. profiles as unknown as { display_name : string } | null )?. display_name ?? 'Unknown' 7. Prevent Vercel Caching Stale Financial Data // Add to every server component/page showing live financial data export const dynamic = 'force-dynamic' For admin financial stats, use direct table queries — RPC results can be cached by Vercel: const [topups, settledBets, winTxns] = await Promise . all ([ admin. from ( 'transactions' ). select ( 'amount' ). eq ( 'type' , 'topup' ), admin. from ( 'bets' ). select ( 'amount' ). in ( 'status' , [ 'won' , 'lost' ]), admin. from ( 'transactions' ). select ( 'amount' ). eq ( 'type' , 'win' ), ]) const totalCashIn = (topups. data ?? []). reduce ( ( s, r ) => s + Number (r. amount ), 0 ) const totalStaked = (settledBets. data ?? []). reduce ( ( s, r ) => s + Number (r. amount ), 0 ) const totalPaidOut = (winTxns. data ?? []). reduce ( ( s, r ) => s + Number (r. amount ), 0 ) 8. Per-Match Leaderboard + Fresh Dropdown Don't rely on server-rendered props for dynamic lists. Fetch client-side on interaction: useEffect ( () => { if (tab !== 'match' ) return fetch ( '/api/matches' ). then ( r => r. json ()). then (setLiveMatches) }, [tab]) Per-match leaderboard — use admin client, join through markets filtered by match_id: const { data } = await admin. from ( 'bets' ) . select ( 'user_id, amount, status, payout, profiles(display_name), markets!inner(match_id)' ) . eq ( 'markets.match_id' , matchId) // aggregate per user_id -> staked, winnings, net P&L, sort by net desc 9. BetSlip Live Preview + Early Bird Banner // Live odds update as user types stake useEffect ( () => { const num = parseFloat (amount) if (! isNaN (num) && num > 0 ) setPreviewOdds ( calculateOdds (market. bet_options , selectedOption. id , num, market. house_edge_pct )) else setPreviewOdds ( null ) }, [amount]) // Early bird window check (30 min from market creation) const isEarlyBird = marketCreatedAt ? new Date () < new Date ( new Date (marketCreatedAt). getTime () + 30 * 60 * 1000 ) : false {isEarlyBird && ( < div className = "bg-yellow-500/10 border border-yellow-500/30 rounded-lg px-3 py-2 text-xs text-yellow-400" > Early bird! Bet now for a +10% bonus on your payout. </ div > )} 10. Lazy Bettor Loading Only fetch bettors when a market is expanded — don't load all on page mount: function toggleExpand ( marketId : string ) { setExpandedMarkets ( prev => { const next = new Set (prev) if (next. has (marketId)) { next. delete (marketId) } else { next. add (marketId) if (!bettors[marketId]) fetchBettors (marketId) // lazy } return next }) } 11. Admin Role Check in API Routes Always read role from the database — JWT claims are cached and won't reflect immediate changes: const { data : profile } = await supabase. from ( 'profiles' ). select ( 'role' ). eq ( 'id' , user. id ). single () if (profile?. role !== 'admin' ) return NextResponse . json ({ error : 'Forbidden' }, { status : 403 }) 12. Common Pitfalls Pitfall Fix No force-dynamic Vercel serves stale balances/odds — add to every financial page Role from JWT JWT is cached — always read role from profiles table per request Client-side odds only Players can manipulate — recalculate server-side in bet API No FOR UPDATE in RPC Race conditions overdraw wallets Anon client for leaderboard RLS blocks it — use admin client server-side Supabase join types Vercel build fails — cast with as unknown as ExpectedType Load all bettors on mount Slow with many markets — lazy-fetch only when expanded 13. Environment Variables NEXT_PUBLIC_SUPABASE_URL= # Safe for client
Agent 识别该技能的关键词,点击任意一个即可复制。

该技能未提供触发词。

下载的 .skill 包内含以下字段。
字段 说明
format格式标识(skill/v1)
skill_id技能唯一 ID
name技能名称
version版本号
description技能描述
category所属分类(数组)
trigger_words触发词列表
tags标签列表
source来源标识
source_url来源链接(本页地址)
exported_at导出时间(每次下载生成)
system_prompt系统提示词正文
model_config模型参数:provider / model / temperature / max_tokens / top_p
examples示例
install_guide各平台导入说明(Coze / Dify / Claude / 自定义框架)
同一份技能可按不同平台格式导出。
.skill 标准格式,含 system_prompt 与 model_config,导入任意 Agent 框架即可使用 下载
.skillpro 增强格式,额外含脚本 / 工具 / 依赖 / 钩子占位 下载
.json 纯 JSON 导出,只含 system_prompt 与模型参数 下载
Coze 带 frontmatter 的 Markdown,Coze 平台导入用 下载
Dify Dify DSL,创建应用后直接导入 下载

每日精选 Skill 推荐,免费送到你邮箱

输入邮箱,每天接收一个精选 AI Agent 技能推荐。完全免费,持续更新。

验证码 --

提交后我们会发送一封确认邮件,点击邮件里的链接才会开始收信。

完全免费,取消任意时间。我们不会发送垃圾邮件。