Skip to main content
POST
/
api
/
arena
/
model-vote
curl -X POST 'http://localhost:5079/api/arena/model-vote' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "comparisonId": "c7d3a4b2-9e1f-4c5d-8b3a-7f6e9d2c1a0b",
    "voteChoice": "left"
  }'
{
  "success": true,
  "message": "Vote recorded successfully"
}

Authentication

Required: JWT Bearer token JWT Claims Extraction (Lines 51-55):
sub | ClaimTypes.NameIdentifier → User UUID (optional, fallback if userId not in body)

Request Body

comparisonId
string
required
Comparison UUID from dual-chat responseValidation (Lines 26-34):
if (request == null || request.ComparisonId == Guid.Empty) {
    return BadRequest("ComparisonId is required");
}
Constraints:
  • MUST NOT be empty GUID (00000000-0000-0000-0000-000000000000)
  • MUST be valid GUID format
  • Links to comparisons table record
voteChoice
string
required
Vote selectionValidation (Lines 36-44):
if (string.IsNullOrWhiteSpace(request.VoteChoice)) {
    return BadRequest("VoteChoice is required (left, right, tie, both-bad)");
}
Allowed Values (Line 107):
  • "left": Vote for left/agent1 model
  • "right": Vote for right/agent2 model
  • "tie": Both models equally good
  • "both-bad": Both models equally bad
Case Handling: Automatically lowercased (Line 61)Invalid Values: Not validated by controller (service validation behavior not enforced by server contract)
userId
string
User UUID (optional)Fallback Logic (Lines 48-56):
Guid? userId = request.UserId;
if (!userId.HasValue) {
    // Extract from JWT claims
    userId = parsedId from JWT;
}
Priority:
  1. Use userId from request body if provided
  2. Fall back to JWT sub claim if not provided
Behavior: Optional in body, automatically extracted from auth token
winnerModelName
string
deprecated
DEPRECATED (Line 111)Kept for backwards compatibility, not usedReplacement: Use voteChoice instead
curl -X POST 'http://localhost:5079/api/arena/model-vote' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "comparisonId": "c7d3a4b2-9e1f-4c5d-8b3a-7f6e9d2c1a0b",
    "voteChoice": "left"
  }'
{
  "success": true,
  "message": "Vote recorded successfully"
}

Side Effects

Database Mutations (Lines 59-63):
await _modelStatsService.RecordVoteByChoiceAsync(
    request.ComparisonId, 
    request.VoteChoice.ToLower(), 
    userId
);
Tables Written:
  1. model_votes table (INSERT):
    • comparison_id → request.ComparisonId
    • vote_choice → request.VoteChoice (lowercased)
    • user_id → userId (from body or JWT)
    • created_at → NOW()
  2. Potential CASCADE UPDATES (service-level, not in controller):
    • ai_models table: Win/loss count updates not enforced by server contract
    • comparisons table: Winner field update not enforced by server contract

Authorization

Authentication: Required (JWT Bearer) Ownership: No ownership check
  • Any authenticated user can vote on any comparison
  • No verification that user created the comparison
Vote Uniqueness: Not enforced by controller
  • Multiple votes on same comparison allowed
  • Deduplication logic (if any) in service layer

Permissions

Who Can Vote:
  • Any authenticated user
  • User who created comparison
  • Users who didn’t create comparison
Who Cannot Vote:
  • Unauthenticated users
Restrictions: None documented in controller

Edge Cases

  1. Empty GUID comparison_id: 400 error (Lines 26-34)
  2. Null voteChoice: 400 error (Lines 36-44)
  3. Empty voteChoice: 400 error (Lines 36-44)
  4. Whitespace-only voteChoice: 400 error (Lines 36-44)
  5. Invalid voteChoice (not in enum): Behavior not enforced by server contract (service validation assumed)
  6. Comparison doesn’t exist: Service exception → 500 error
  7. Multiple votes on same comparison: Allowed (no uniqueness check in controller)
  8. Vote on own comparison: Allowed
  9. userId in body but different from JWT: Body value used (Lines 48-49)
  10. No userId in body, no JWT claim: userId = null, passed to service

Error Conditions

CodeHTTPCauseController Line
N/A401JWT missing or invalidMiddleware
INVALID_REQUEST400ComparisonId null or empty GUID26-34
INVALID_REQUEST400VoteChoice null/empty/whitespace36-44
VOTE_ERROR500Service exception71-79
Exception Handling (Lines 71-79):
catch (Exception ex) {
    return StatusCode(500, new { error = ex.Message, code = "VOTE_ERROR" });
}
  • All service exceptions return 500
  • Exception message exposed to client
Possible Service Exceptions:
  • “Comparison not found”
  • “Invalid vote choice”
  • “Database constraint violation”

Vote Choice Semantics

"left" (Lines 59-63)

  • Votes for model in agent1 position
  • Win count increment behavior not enforced by server contract
  • Votes for model in agent2 position
  • Win count increment behavior not enforced by server contract

"tie"

  • Both models equally good
  • Tie count increment behavior not enforced by server contract

"both-bad"

  • Both models equally bad
  • Loss count increment behavior not enforced by server contract

Behavioral Guarantees

Atomicity: Database transaction not enforced by controller (service-dependent) Idempotency: NOT idempotent
  • Each call creates new vote record
  • No deduplication
Vote Timing: No expiration check
  • Can vote on old comparisons
  • No time limit enforced

Comparison Validation

Existence Check: Not in controller code
  • Assumed to be in service layer
  • If comparison doesn’t exist, service throws exception → 500
Ownership: Not checked
  • Any user can vote on any comparison

Vote Storage

Winner Model Resolution (Line 59):
RecordVoteByChoiceAsync(comparisonId, voteChoice, userId)
Service Responsibility:
  • Lookup comparison by comparisonId
  • Resolve voteChoice (“left”/“right”) to actual model name
  • Store vote with model reference
Controller Does NOT:
  • Validate comparison exists
  • Resolve model names
  • Check duplicate votes

Authentication Fallback

Priority Order (Lines 48-56):
  1. request.UserId (if provided and valid GUID)
  2. JWT sub claim
  3. JWT ClaimTypes.NameIdentifier claim
  4. null (if none available)
Note: Passing null userId to service may cause service-level error (not documented)

Validation Order

  1. Request body null check
  2. ComparisonId validation (400 if invalid)
  3. VoteChoice validation (400 if invalid)
  4. UserId extraction (body or JWT)
  5. Service call (500 if fails)
Case Normalization: VoteChoice lowercased before service call (Line 61)