feat(admin): operations console, fees wired into payouts
Fees now flow through settlement. The payout and the deduction are posted as separate ledger transactions rather than netted, so a player's history shows the full win and the charge as itemised lines instead of a quietly smaller win. The admin console shows treasury, liability, revenue, every posting, every round, and risk flags. Auth is a constant-time token compare and the surface is not mounted at all unless ARCADE_ADMIN_TOKEN is set, so a default deployment has no admin endpoint to attack. The token lives in browser memory only. It is read-only over game outcomes by design: seeds show only after settlement and nothing can alter a crash point. A control that could would make the fairness proof a lie. The console immediately found a real bug: 343 unresolved rounds, because the reconciler only considered rounds with bets and abandoned empty ones accumulated forever, burying the signal. Now cleared automatically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -21,11 +22,13 @@ import (
|
||||
"github.com/coder/websocket"
|
||||
"github.com/drjones/quantum-arcade/pkg/cluster"
|
||||
"github.com/drjones/quantum-arcade/pkg/fair"
|
||||
"github.com/drjones/quantum-arcade/pkg/fees"
|
||||
"github.com/drjones/quantum-arcade/pkg/fixed"
|
||||
"github.com/drjones/quantum-arcade/pkg/identity"
|
||||
"github.com/drjones/quantum-arcade/pkg/ledger"
|
||||
"github.com/drjones/quantum-arcade/pkg/room"
|
||||
"github.com/drjones/quantum-arcade/pkg/scratch"
|
||||
"github.com/drjones/quantum-arcade/pkg/sim"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
@@ -184,6 +187,11 @@ func (s *server) routes() http.Handler {
|
||||
}
|
||||
mux.HandleFunc("GET /ws/{game}", s.handleWS)
|
||||
mux.HandleFunc("GET /api/cluster", s.handleCluster)
|
||||
mux.HandleFunc("GET /api/fees", s.handleFees)
|
||||
|
||||
// Mounted only when ARCADE_ADMIN_TOKEN is set, so a default deployment
|
||||
// has no admin surface at all.
|
||||
s.routesAdmin(mux)
|
||||
|
||||
sub, err := fs.Sub(staticFiles, "static")
|
||||
if err != nil {
|
||||
@@ -608,6 +616,32 @@ func (s *server) handleScratchPlay(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleFees publishes exactly what the operator takes. It is generated from
|
||||
// the same schedule the code charges, so the published terms cannot drift from
|
||||
// the behaviour.
|
||||
func (s *server) handleFees(w http.ResponseWriter, r *http.Request) {
|
||||
sch := fees.DefaultSchedule()
|
||||
tickets := make([]map[string]any, 0, len(scratch.Catalog))
|
||||
for _, t := range scratch.Catalog {
|
||||
tickets = append(tickets, map[string]any{
|
||||
"ticket": t.Name,
|
||||
"game_rtp_percent": fmt.Sprintf("%.2f%%", float64(t.RTPBasisPoints())/100),
|
||||
"effective_percent": fmt.Sprintf("%.2f%%",
|
||||
float64(sch.EffectiveRTPBasisPoints(int64(t.RTPBasisPoints())))/100),
|
||||
})
|
||||
}
|
||||
crashRTP := int64(10000 - sim.HouseEdgeBP)
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"schedule": sch.Describe(crashRTP),
|
||||
"crash_games": map[string]string{
|
||||
"game_rtp_percent": fmt.Sprintf("%.2f%%", float64(crashRTP)/100),
|
||||
"effective_percent": fmt.Sprintf("%.2f%%",
|
||||
float64(sch.EffectiveRTPBasisPoints(crashRTP))/100),
|
||||
},
|
||||
"scratch_tickets": tickets,
|
||||
})
|
||||
}
|
||||
|
||||
// handleCluster reports the instances currently serving and which of them
|
||||
// drives each game. This is the operator's view of a cloned fleet.
|
||||
func (s *server) handleCluster(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user