Predictu
Users & Auth

Authentication

Predictu supports three authentication flows depending on how the user accesses the platform: email/password login for returning users, email/password registration for new users, and embed initialization for users arriving through a casino operator’s iframe integration. All flows produce a JWT that is used for subsequent API requests.

Stateless sessions: Predictu uses JWT-based authentication. There is no server-side session store. The JWT contains all the information needed to identify and authorize the user. Tokens are validated on every request via middleware.

Flow 1: Email/Password Login

Standard login for users who already have an account. Used in both the standalone Predictu app and operator-branded embed modes (when the operator allows direct login).

POST/api/auth/login

Request Body

{
  "email": "user@example.com",
  "password": "securepassword123"
}

Flow Steps

1
Validate input - Check that both email andpassword are present and properly formatted.
2
Look up user - Query the users table by email. If no user is found, return a generic error (do not reveal whether the email exists).
3
Check ban status - If is_banned = true on the user record, reject the login with a 403 Forbidden response.
4
Verify password - Compare the provided password against the stored bcrypt hash. If it does not match, return a generic authentication error.
5
Generate JWT - Create a signed JWT containing the user’s ID, email, operator ID, and role.
6
Return response - Send the JWT and user profile back to the client.

Success Response (200)

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "usr_abc123",
    "email": "user@example.com",
    "display_name": "JohnD",
    "balance": 9850.00,
    "tier": "regular",
    "operator_id": null,
    "created_at": "2026-01-15T10:30:00Z"
  }
}

Error Responses

StatusCodeDescription
400INVALID_INPUTMissing or malformed email/password.
401AUTH_FAILEDInvalid email or password (generic to prevent enumeration).
403USER_BANNEDAccount has been banned by an admin.
Security note: The login endpoint uses the same error message for “user not found” and “wrong password” to prevent email enumeration attacks. The response is always AUTH_FAILED for both cases.

Flow 2: Email/Password Registration

Creates a new user account with an initial starting balance. Used for self-service sign-up on the standalone platform.

POST/api/auth/register

Request Body

{
  "email": "newuser@example.com",
  "password": "securepassword123",
  "display_name": "NewUser42"
}

Flow Steps

1
Validate input - Check email format, password strength (minimum 8 characters), and display name length (3–30 characters).
2
Check for duplicates - Query users by email. If the email already exists, return a 409 Conflict error.
3
Hash password - Generate a bcrypt hash of the password with a cost factor of 12.
4
Create user record - Insert into users withbalance = 10000.00 (the $10k starting balance),tier = 'new', and operator_id = null.
5
Set starting balance - Send an initial balance callback to the operator’s S2S server to record the $10,000 starting balance for the new player.
6
Generate JWT - Same JWT generation as the login flow.
7
Return response - Send the JWT and new user profile to the client.

Success Response (201)

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "usr_xyz789",
    "email": "newuser@example.com",
    "display_name": "NewUser42",
    "balance": 10000.00,
    "tier": "new",
    "operator_id": null,
    "created_at": "2026-03-19T14:22:00Z"
  }
}

Error Responses

StatusCodeDescription
400INVALID_INPUTMissing fields, weak password, or invalid email format.
409EMAIL_EXISTSAn account with this email already exists.
Starting balance: The $10,000 starting balance is a platform default for the demo/staging environment. In production, this value is configurable per operator and may be $0 for real-money deployments where users deposit their own funds.

Flow 3: Embed Initialization

This is the authentication flow used when Predictu is embedded inside a casino operator’s site via an iframe. The operator sends user information through the PostMessage bridge, and Predictu creates or finds a matching user account scoped to that operator.

POST/api/auth/embed-init

Request Body

{
  "operator_id": "op_example",
  "user_id": "player_12345",
  "display_name": "HighRoller99",
  "token": "operator-signed-jwt-token"
}

Flow Steps

1
Validate operator token - Verify the operator’s JWT using their registered s2s_secret. This confirms the request is genuinely from the operator and not forged.
2
Load operator config - Fetch the operator record to get branding settings, integration mode, and allowed origins.
3
Generate synthetic email - Create a deterministic email address in the format: {operator_id}-{user_id}@embed.predictu.io. This scopes the user to the operator and avoids collisions with real email addresses.
4
Find or create user - Look up a user with the synthetic email. If found, update the display name if changed. If not found, create a new user with the synthetic email, the operator’s default starting balance, andoperator_id set to the operator.
5
Check ban status - If the user is banned, return403 Forbidden.
6
Generate JWT - Create a JWT that includes theoperator_id claim, which scopes all subsequent API calls.
7
Return response with branding - Send the JWT, user profile, and operator branding configuration back to the iframe.

Success Response (200)

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "usr_emb_456",
    "email": "op_example-player_12345@embed.predictu.io",
    "display_name": "HighRoller99",
    "balance": 10000.00,
    "tier": "new",
    "operator_id": "op_example",
    "created_at": "2026-03-19T14:30:00Z"
  },
  "branding": {
    "primary_color": "#00b341",
    "logo_url": "https://cdn.example-casino.com/logo.png",
    "platform_name": "Example Casino Predictions",
    "hide_predictu_branding": true
  }
}

Synthetic Email Format

The synthetic email is the key to operator-scoped user isolation. It follows a strict format that is deterministic and collision-free:

// Format: {operator_id}-{user_id}@embed.predictu.io

// Examples:
"op_example-player_12345@embed.predictu.io"
"op_betworld-user_98765@embed.predictu.io"
"op_luckycasino-uid_abc@embed.predictu.io"

// The same operator + user_id always produces the same email,
// so subsequent logins find the existing account.
Operator isolation: A user from operator A and a user from operator B with the same external user_id will get different Predictu accounts because the operator ID is part of the synthetic email. There is no cross-operator data leakage.

JWT Structure

All three authentication flows produce JWTs with the same structure. The token is signed with HS256 using the platform’s JWT_SECRET environment variable.

JWT Payload

{
  "sub": "usr_abc123",          // User ID (subject)
  "email": "user@example.com",  // User email (real or synthetic)
  "operator_id": "op_example",  // Operator scope (null for direct users)
  "role": "user",               // "user" | "admin" | "operator_admin"
  "tier": "regular",            // Current user tier
  "iat": 1710856800,            // Issued at (Unix timestamp)
  "exp": 1710943200             // Expires at (24h from issuance)
}

Field Reference

ClaimTypeDescription
substringThe Predictu user ID. Primary identifier for all API operations.
emailstringReal email for direct users, synthetic for embed users.
operator_idstring | nullScopes the user to an operator. Null for direct Predictu users.
rolestringDetermines access level. Regular users are always user.
tierstringCached tier for quick access. May be stale if tier changes mid-session.
iatnumberUnix timestamp when the token was issued.
expnumberUnix timestamp when the token expires (24 hours after issuance).

Session Management

Predictu uses stateless JWT sessions. The token is stored client-side (in memory for the embed, in localStorage for the standalone app) and sent as aBearer token in the Authorization header on every API request.

Request Validation

// Middleware validates every API request
async function authMiddleware(request: NextRequest) {
  const token = request.headers
    .get("Authorization")
    ?.replace("Bearer ", "");

  if (!token) {
    return NextResponse.json(
      { error: "AUTH_REQUIRED" },
      { status: 401 }
    );
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!);

    // Check if user is banned (fresh check, not from JWT)
    const user = await db.getUser(payload.sub);
    if (user.is_banned) {
      return NextResponse.json(
        { error: "USER_BANNED" },
        { status: 403 }
      );
    }

    // Attach user context to request
    request.user = payload;
    return NextResponse.next();
  } catch (err) {
    return NextResponse.json(
      { error: "INVALID_TOKEN" },
      { status: 401 }
    );
  }
}
Fresh ban check: The middleware performs a live database check foris_banned on every request, even though the JWT is otherwise self-contained. This ensures that a banned user is immediately locked out, even if their JWT has not yet expired. This is the one place where the “stateless” design makes a deliberate exception.

Token Lifecycle

EventAction
Login / Register / Embed-initNew JWT issued with 24h expiry
Token expiresClient must re-authenticate (login or embed-init)
User bannedExisting JWT rejected at middleware on next request
Tier changedJWT tier claim is stale; fresh tier loaded from DB for risk checks
Page refresh (embed)Operator re-sends embed-init via PostMessage; new JWT issued

Ban Checking

The is_banned flag on the user record is the master switch for account access. When set to true, the user is immediately locked out of the platform.

Effects of Banning

  • Login blocked - The login flow checks is_bannedbefore generating a JWT. Banned users receive 403 USER_BANNED.
  • Embed-init blocked - Same check applies to the embed flow. The iframe will show an error state.
  • API requests blocked - The middleware performs a live ban check on every request. An existing JWT is immediately invalidated.
  • Open positions preserved - Banning does not close or void a user’s positions. They will still be settled normally when markets resolve. The user simply cannot place new trades or access the platform.
Unbanning: Setting is_banned = false via the admin dashboard immediately restores access. The user can log in again and their account, balance, and positions are all intact.

Embed Auth: Operator-Scoped Users

The embed initialization flow is the most complex of the three because it bridges two identity systems: the operator’s (external user IDs) and Predictu’s (internal user IDs).

Identity Mapping

// Operator sends via PostMessage:
{
  type: "PREDICTU_INIT",
  operator_id: "op_example",
  user_id: "player_12345",      // Operator's user ID
  display_name: "HighRoller99",
  token: "operator-jwt"
}

// Predictu maps this to:
{
  email: "op_example-player_12345@embed.predictu.io",  // Synthetic
  operator_id: "op_example",
  display_name: "HighRoller99",
}

// On first visit: creates new Predictu user with this mapping
// On return visit: finds existing user by synthetic email

Security Considerations

  • Origin validation - The PostMessage handler checks the origin of incoming messages against the operator’s registered allowed origins. Messages from unregistered origins are silently ignored.
  • Operator JWT verification - The token field is a JWT signed with the operator’s s2s_secret. Predictu verifies this signature before processing the init request. This prevents unauthorized user creation.
  • No password - Embed users do not have a password. They cannot log in via the standard login flow. Their only entry point is through the operator’s embed.
  • Synthetic emails are not routable - The@embed.predictu.io domain does not receive email. Synthetic emails are purely an internal identifier scheme.

The three auth flows are designed to be mutually compatible. A direct Predictu user and an embed user can both trade on the same markets. The only difference is how they authenticate and which wallet adapter handles their balance.