Zum Inhalt

05 - Buy Flow (Ticket-Kauf)

Dokument: Meta-Transaction Ticket-Kauf-System Stand: 28.02.2026 Scope: Intent-basierter Kauf via Relayer Status: Audit-ready


1. Übersicht

Der Ticket-Kauf verwendet ein Meta-Transaction-System: Nutzer signieren EIP-712 Permits, der Relayer sendet die Transaktion on-chain. Nutzer zahlen kein ETH-Gas direkt - Gaskosten werden in USDT verrechnet.

Beteiligte Systeme

System Rolle
Frontend (useBuyFlow) UI, Signierung, Status-Tracking
Backend (API Routes) Intent-Verwaltung, Relayer-Steuerung
Relayer (Wallet) On-Chain TX-Submission
GameManager (Contract) Ticket-Erstellung on-chain
ReserveVault (Contract) USDT-Pull (Approval-Empfänger)

2. Ablaufdiagramm

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Intent     │────▶│  Signatur   │────▶│  Approval   │────▶│   Submit    │
│  erstellen   │     │  (EIP-712)  │     │   (USDT)    │     │  (Relayer)  │
│ /buy/intent  │     │  Client     │     │  On-Chain   │     │ /buy/submit │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
      ┌─────────────┐     ┌─────────────┐                          │
      │  Ergebnis   │◀────│  Bestätig.  │◀─────────────────────────┘
      │  anzeigen   │     │  /confirm   │
      └─────────────┘     └─────────────┘

3. Detaillierter Flow

Schritt 1: Intent erstellen

Endpoint: POST /api/buy/intent

Request:

{
  "buyer": "0x...",
  "chainId": 421614,
  "poolId": 0-4,
  "tips": [[1,2,3,4,5,6], [3,7,15,22,30,38]],
  "paidTipCount": 2,
  "bonusTipCount": 0
}

Response:

{
  "intentId": "uuid",
  "permit": {
    "buyer": "0x...", "poolId": 2,
    "tipsHash": "0x...", "deadline": 1234567890,
    "nonce": 1, "maxGasCostUsdt": "50000"
  },
  "typedData": { "domain": {}, "types": {}, "message": {} },
  "pricePerTip": "1000000",
  "ticketCost": "2000000",
  "gasCostUsdt": "50000",
  "totalCost": "2050000",
  "tipCount": 2, "bonusTipCount": 0,
  "autoBonus": 0, "extraBonus": 0,
  "affiliate": "0x...",
  "needsGasFaucet": true,
  "faucetCostUsdt": "25000"
}

Backend-Prüfungen: - Maintenance-Modus (chain_stats) - Spending Limits (daily/weekly/monthly) - Self-Exclusion Status - Tip-Validierung (6 Zahlen, 1-40, unique) - Extra-Bonus-Voucher prüfen - Affiliate-Rate laden - ETH-Balance prüfen (für Gas-Faucet-Bedarf) - USDT-Allowance prüfen

Fehler: - 400: Ungültige Eingabe, Tip-Mismatch, Limit überschritten, Minderjährig - 403: Self-excluded, Limit überschritten, Unzureichende Allowance - 409: TX bereits in Ausführung (EXECUTING Status) - 503: Maintenance

Schritt 2: EIP-712 Signatur

Client-seitig via ethers.js Signer

BuyPermit-Struktur:

Domain: { name: "ChainbetsGame", version: "1", chainId, verifyingContract }
Types: { BuyPermit: [buyer, poolId, tipsHash, deadline, nonce, maxGasCostUsdt] }

Endpoint (Bestätigung): POST /api/buy/confirm-signature

Request:  { "intentId": "uuid", "nonce": "0x...", "signature": "0x..." }
Response: { "ok": true, "status": "SIGNED", "signatureHash": "0x...",
            "extraBonusProof": "0x...", "extraBonusAmount": 3 }

Backend-Logik: - Signer aus EIP-712 Signatur recovern - Extra-Bonus-Proof generieren (BONUS_SIGNER_PK) - keccak256(abi.encode(buyer, extraBonus, nonce))

Schritt 3: USDT Approval (falls nötig)

Prüfung: Aktuelle Allowance vs. benötigter Betrag

Falls Allowance < Kosten:
  → USDT.approve(RESERVEVAULT_ADDRESS, MAX_UINT256)
  → Gas-Fee: 50% Buffer auf Base Fee
  → Gas-Faucet falls kein ETH vorhanden

Gas-Faucet Endpoint: POST /api/buy/gas-faucet

Request:  { "intentId": "uuid", "wallet": "0x..." }
Response: { "ok": true, "status": "FUNDED", "txHash": "0x...", "amountWei": "..." }

Anti-Abuse: | Regel | Wert | |---|---| | Max. unrecouped Claims | 20 | | Max. pro Stunde | 50 | | Nur Web3Auth-Wallets | Ja | | Max. 1 Claim/Wallet | Bis recouped |

Schritt 4: Transaktion senden

Endpoint: POST /api/buy/submit-tx

Request:  { "intentId": "uuid", "signature": "0x..." }
Response: { "ok": true, "status": "TX_CONFIRMED", "txHash": "0x...",
            "blockNumber": 12345, "gasUsed": "150000" }

Backend-Logik: - Nonce-Manager für Sequenzierung - Dynamische Gas-Price-Berechnung mit Buffer - Force-Sync bei TX-Send-Fehler - Warten auf Receipt-Bestätigung

Contract-Call:

GameManager.buyTicketFor(
  buyer, tipCount, bonusTipCount, affiliate,
  permit, signature, extraBonusProof, gasCostUsdt
)

Schritt 5: Bestätigung

Endpoint: POST /api/buy/confirm-tx

Request:  { "intentId": "uuid", "txHash": "0x..." }
Response: {
  "ok": true, "status": "CONFIRMED",
  "result": {
    "ticketId": 42, "dayId": 100,
    "rngNumbers": [5,12,18,23,34,40],
    "matchesPerTip": [3, 1],
    "totalMatches": 4
  },
  "amounts": { "tipCost": "2000000", "gasCost": "50000" },
  "bonus": { "autoBonus": 0, "extraBonus": 0 }
}

Backend-Logik: - TX Receipt abrufen - TicketCreated + Deposited Events parsen - Treffer für jeden Tip OFF-CHAIN berechnen - Tickets und ticket_tips in DB einfügen - Extra-Bonus-Voucher für 3-Treffer-Gewinne erstellen - Bestehende Extra-Bonus-Voucher konsumieren (FIFO)

Schritt 6: Ergebnis anzeigen

  • RevealScreen mit Zahlen-Animation
  • Treffer hervorheben
  • Confetti bei Gewinnen (3-stufig)
  • Balance-Refresh

4. Bonus-System

4.1 Auto-Bonus (Deterministic)

Formel: Tips < 5 → 0, Tips >= 5 → 3 + floor((Tips-5)/5) × 4
Tips Bonus Gesamt
1-4 0 1-4
5-9 3 8-12
10-14 7 17-21
15-19 11 26-30
20-24 15 35-39

4.2 Extra-Bonus (Voucher-basiert)

Quelle Tips
3-Treffer-Gewinn 3 Bonus-Tips
Kein 3+ Treffer 1 Trost-Tip
Profil-Bonus 10 Tips
Login-Streak (7 Tage) 5-8 Tips
Leaderboard Variabel

4.3 Bonus-Proof-Generierung

Backend signiert mit BONUS_SIGNER_PK:
hash = keccak256(abi.encode(buyer, extraBonusAmount, nonce))
proof = BONUS_SIGNER.signMessage(hash)

5. Intent-Recovery

Endpoint: POST /api/buy/resume-intent

Bei Seiten-Reload oder Verbindungsabbruch:

1. Intent-ID aus State/LocalStorage laden
2. POST /api/buy/resume-intent { intentId }
3. Backend gibt aktuellen Intent-Status zurück
4. Flow an passender Stelle fortsetzen

Status-Mapping: | Intent-Status | Frontend-Aktion | |---|---| | CREATED | Erneut signieren | | SIGNED | Weiter mit Submit | | EXECUTING | Warten auf Bestätigung | | CONFIRMED | Ergebnis anzeigen | | FAILED | Fehler anzeigen | | EXPIRED | Neuen Intent erstellen |


6. Fehlerbehandlung

6.1 Pre-Flight-Checks

Check Aktion bei Fehler
USDT-Balance zu niedrig error_balance State
Settlement nicht abgeschlossen error_unsettled State
Tages-/Wochen-/Monatslimit error_limit State
Self-Exclusion aktiv error_self_excluded State
TX bereits in Ausführung error_tx_in_progress State

6.2 Runtime-Fehler

Fehler State Recovery
User lehnt Signatur ab signature_cancelled Reset, erneut versuchen
Approval fehlgeschlagen error_rejected Reset
Gas-Faucet Fehler Non-fatal Flow wird fortgesetzt
TX-Submission Fehler error_network Resume-Intent
Bestätigungs-Timeout (30s) error_timeout Resume-Intent
Permit abgelaufen error_expired Neuen Intent erstellen

6.3 Confirmation-Polling

Timeout: 30 Sekunden
Intervall: 1 Sekunde
Fallback: Stündliches Polling nach Timeout

7. Nonce-Manager

7.1 V1 (Spinlock-basiert)

Parameter Wert
Lock-Mechanismus Spinlock mit 10ms Sleep
Timeout 120 Sekunden
Netzwerk-Sync Alle 30 Sekunden

7.2 V2 (Queue-basiert, Production)

Parameter Wert
Queue-System Async mit Promise-Resolution
Rate-Limiting 50ms Minimum zwischen Allokationen
Queue-Timeout 30 Sekunden
Gap-Detection Warnung bei > 50 Nonces voraus
Stale-Cleanup Automatisch

Monitoring: GET /api/admin/nonce-status


8. Kostenstruktur

Gesamtkosten = (paidTipCount × pricePerTip) + gasCostUsdt + faucetCostUsdt
Komponente Beschreibung
Tip-Kosten Anzahl bezahlter Tips × Pool-Preis
Gas-Kosten Geschätzte Relayer-Gaskosten in USDT
Faucet-Kosten Falls ETH für Approval nötig (einmalig)

USDT-Precision: 6 Decimals (1 USDT = 1.000.000 Units)


Weiterführende Dokumente: - 06 - Claim Flow - 07 - API Referenz - 08 - Blockchain