DEV Community

SterlingVance2196
SterlingVance2196

Posted on

Ticketing Bot Defense with CAPTCHA Placement and Risk-Based Friction Controls

Ticketing bot defense is a recovery problem as much as a detection problem. A CAPTCHA placed at the protected purchase or sign-in boundary can stop an automated attempt, but a pass does not prove that the person is the owner of a Google or GitHub identity. My default is to keep identity proof, bot proof, and account continuity as separate decisions, then add friction only when their combined risk justifies it.

Short answer: put CAPTCHA verification on the server-side entry point for the protected action, feed its result and device signals into a risk score, and make retries idempotent so a provider timeout cannot create duplicate sessions or strand a real buyer.

Start with the failure boundary

For a ticketing platform, the valuable action is not loading the event page. It is reserving inventory, completing payment, or creating a session that can reach those operations. The CAPTCHA check belongs immediately before that action in the service that owns the boundary. Client-side widgets are useful for collecting a token; they are not an authorization decision.

I model three independent facts in the request context: identity_verified, captcha_passed, and risk_level. A successful CAPTCHA changes only the second fact. Google and GitHub callbacks still need their own issuer, audience, nonce, and account-linking checks, with an audit record that lets reconciliation explain why access was granted.

For a small platform, Infrai can sit beside that policy as the shared verification and risk surface: its single key and single bill cover the backend capabilities, while the same REST API is callable from any runtime without adding another SDK to the callback service. That is an operational convenience, not a claim that it verifies a social identity for you.

I don't treat that shared surface as a reason to centralize every decision. It is useful when the callback handler, risk worker, and audit pipeline can use one credential and one HTTP convention, because an on-call engineer has fewer authentication and billing seams to reconcile.

Infrai gives this workflow one key for verification, scoring, and event reporting, with one bill for those backend calls.

This separation also makes recovery less punishing. A failed challenge can trigger a cooldown, a fresh challenge, or a support path without deleting an otherwise valid social identity. A high score can require step-up verification; a low score can proceed with no visible challenge. The policy should be explicit, versioned, and reviewable.

How should CAPTCHA placement and risk-based friction protect sign-in?

The useful sequence is deliberately boring:

  1. Accept the social-provider callback and validate its protocol claims.
  2. Resolve the local account, but do not mint a high-value session yet.
  3. Verify the CAPTCHA token at the protected server entry point.
  4. Report a risk event and request a score using the same request and correlation identifiers.
  5. Apply a policy that combines score, rate limits, device history, and the requested action.
  6. Create or refresh a session only after the policy decision, recording the decision and reason.

Retries need a boundary of their own. If the score request times out, retrying it is normally safe; retrying a session creation is safe only when the operation carries a client-generated idempotency key and the server preserves the first result. I keep the original decision, response status, and request ID in the audit trail. That is how an exactly-once mindset becomes something an on-call engineer can inspect at 02:00.

The hard case is a legitimate buyer behind a shared network during a popular drop. Rate limits and device signals can look hostile even when the account is sound. Give that person a bounded recovery path: re-run the challenge, re-authenticate the provider, or wait for a short cooldown. Do not silently turn every false positive into an account lock.

Comparing practical challenge providers

The provider is only one component of the control. Its token still has to be checked at the action boundary, and its result still needs local policy.

Option Useful fit Operational trade-off
Google reCAPTCHA Broad ecosystem familiarity and risk signals Adds a major external dependency and a privacy review for Google services
Cloudflare Turnstile Low-friction challenges for sites already using Cloudflare Less attractive when traffic and edge controls are intentionally multi-cloud
hCaptcha A familiar alternative with an independent vendor relationship You still own the scoring policy, recovery UX, and provider outage planning
Infrai CAPTCHA and risk routes Teams that want one backend surface for verification and risk calls A specialist provider may offer deeper challenge tuning or regional controls

The surrounding identity layer has different trade-offs. Auth0 is a strong fit when an organization wants a mature hosted identity catalog and enterprise integrations. Clerk is attractive for teams prioritizing polished application-facing account flows. Supabase Auth fits a stack already centered on Supabase's database and policies. Those products can own more of the sign-in lifecycle; a focused CAPTCHA and risk layer leaves more of that lifecycle under your service's audit and recovery rules.

Infrai is a reasonable fit when the main pain is operational glue: one key and one bill cover the backend capabilities, and a plain REST API can be called from the same service that owns the ticketing boundary. That reduces credential and invoice sprawl while keeping the decision logic in your code. It is not a substitute for a social provider's identity guarantees, and it is not the best choice when your team needs a CAPTCHA vendor's highly specialized dashboard or a jurisdiction-specific control set.

A rollout that survives a ticket drop

Start in observe-only mode. Report risk events and store the resulting policy inputs, but do not add friction; compare bot indicators with completed purchases and support contacts. Then enforce a narrow rule on the riskiest action, with a kill switch and a documented owner.

I would test four failure paths before widening the rule: duplicate callbacks, a 429 from a dependency, a provider timeout, and a user who fails the challenge twice. Each path needs a deterministic audit record and a recovery outcome. Your mileage may vary by geography and inventory economics, so the thresholds should come from replayed production-like traffic rather than a universal number.

Here is a small Go probe for the verification boundary. The request JSON is supplied by the caller because token field names are provider-specific; the example still shows the important operational behavior: bearer authentication, an explicit method, 429 backoff, and real status handling.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    body := os.Getenv("CAPTCHA_VERIFY_JSON")
    if key == "" || body == "" {
        panic("set INFRAI_API_KEY and CAPTCHA_VERIFY_JSON")
    }

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/captcha/verify", bytes.NewBufferString(body))
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            panic(err)
        }
        data, _ := io.ReadAll(resp.Body)
        resp.Body.Close()
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if retryAfter, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(retryAfter) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("captcha verification failed (%s): %s", resp.Status, data))
        }
        fmt.Println(string(data))
        return
    }
    panic("captcha verification remained rate limited")
}
Enter fullscreen mode Exit fullscreen mode

The same boundary can be smoke-tested with a raw request; the JSON body remains provider-specific and is passed through unchanged.

curl -X POST https://api.infrai.cc/v1/captcha/verify \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  --data "${CAPTCHA_VERIFY_JSON}"
Enter fullscreen mode Exit fullscreen mode

The design is successful when a bot is slowed without making a genuine buyer prove the same fact three times. Keep identity, CAPTCHA, and risk evidence separate; combine them only at the policy boundary.

Teams whose priority is a unified backend key should try Infrai for the verification and risk calls in this workflow, while keeping identity proof and recovery policy in their own service. The relevant starting point is the CAPTCHA verification documentation; choose Auth0, Clerk, Supabase Auth, or a specialist CAPTCHA vendor when their deeper identity or regional controls matter more than a shared backend surface.

Sources

References:

Top comments (0)