PlayKit.ai

Backend Services

Using PlayKit JS SDK with backend services for server-side AI operations

Backend Services

This guide explains how to use the PlayKit SDK in a client-server architecture, where your backend service needs to make AI calls on behalf of authenticated users.

Architecture Overview

In a typical setup:

  1. Frontend: User authenticates via PlayKit SDK, obtains a Player Token
  2. Backend: Receives the Player Token, validates it, and makes AI calls
┌─────────────┐     Player Token      ┌─────────────┐
│   Frontend  │ ──────────────────▶   │   Backend   │
│  (Browser)  │                       │  (Node.js)  │
└─────────────┘                       └─────────────┘
       │                                     │
       │ Login                               │ Validate Token
       ▼                                     │ + AI Calls
┌─────────────┐                              │
│   PlayKit   │ ◀────────────────────────────┘
│   Server    │
└─────────────┘

Frontend: User Authentication

When no developerToken is provided, the SDK automatically triggers the device authorization flow. This opens a browser window for the user to log in via PlayKit:

import { PlayKitSDK } from 'playkit-sdk';

const sdk = new PlayKitSDK({
  gameId: 'your-game-id'
  // No developerToken = auto triggers device auth flow
});

await sdk.initialize();

// Listen for successful authentication
sdk.on('authenticated', async (authState) => {
  // Get the player token to send to your backend
  const token = sdk.getToken();

  // Send token to your backend for subsequent API calls
  await fetch('/api/set-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ playerToken: token })
  });
});

Features:

  • Opens browser for secure login (email/phone verification)
  • Uses PKCE for security
  • Token automatically saved locally for next session
  • Works in browser and desktop (Electron) environments

Getting the Current Token

Once authenticated, you can retrieve the token anytime:

// Check if authenticated
if (sdk.isAuthenticated()) {
  const token = sdk.getToken();

  // Include in API calls to your backend
  const response = await fetch('/api/your-endpoint', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
}

Backend: Token Validation

Your backend needs to validate Player Tokens before processing requests.

Validating via PlayKit API

Call PlayKit's /api/external/player-info endpoint to validate the token and get user info:

// Node.js / Express example
const PLAYKIT_BASE_URL = 'https://playkit.ai';

async function validatePlayerToken(token, gameId) {
  const response = await fetch(
    `${PLAYKIT_BASE_URL}/api/external/player-info`,
    {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${token}`,
        'X-Game-Id': gameId  // Required for global tokens
      }
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error?.message || 'Token validation failed');
  }

  return await response.json();
  // Returns: { userId, nickname, tokenType, balance, ... }
}

// Express middleware
async function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing authorization header' });
  }

  const token = authHeader.substring(7);

  try {
    const playerInfo = await validatePlayerToken(token, 'your-game-id');
    req.user = playerInfo;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Use in routes
app.post('/api/chat', authMiddleware, async (req, res) => {
  // req.user contains validated player info
  const { userId, balance } = req.user;

  // Check balance before making AI calls
  if (balance < 0.01) {
    return res.status(402).json({ error: 'Insufficient balance' });
  }

  // Proceed with AI call...
});

Player Info Response

The /api/external/player-info endpoint returns:

interface PlayerInfoResponse {
  userId: string;           // User's unique ID
  nickname: string | null;  // Display name
  tokenType: 'player' | 'developer' | 'jwt';
  tokenId: string | null;   // Token ID (null for JWT)
  balance: number;          // Available balance (display currency)
  credits?: number;         // Legacy field, use balance instead
  rechargeMethod?: 'browser' | 'steam' | 'ios' | 'android';
  channelType?: string;     // Distribution channel
  dailyRefresh?: {          // Daily free credits info
    refreshed: boolean;
    message: string;
    balanceBefore?: number;
    balanceAfter?: number;
    amountAdded?: number;
  };
}

Backend: Making AI Calls with User Token

Once validated, use the Player Token to make AI API calls on behalf of the user:

Direct API Calls

const PLAYKIT_BASE_URL = 'https://playkit.ai';

async function chatWithAI(playerToken, gameId, messages, options = {}) {
  const response = await fetch(
    `${PLAYKIT_BASE_URL}/ai/${gameId}/v2/chat`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${playerToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        messages,
        model: options.model || 'gpt-4o-mini',
        temperature: options.temperature || 0.7,
        max_tokens: options.maxTokens || 1000,
        stream: false
      })
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error?.message || 'AI request failed');
  }

  return await response.json();
}

// Usage in Express route
app.post('/api/npc-dialog', authMiddleware, async (req, res) => {
  const { message, npcId } = req.body;
  const playerToken = req.headers.authorization.substring(7);

  try {
    // Get NPC system prompt from your database
    const npc = await getNPCConfig(npcId);

    const result = await chatWithAI(
      playerToken,
      'your-game-id',
      [
        { role: 'system', content: npc.systemPrompt },
        { role: 'user', content: message }
      ],
      { temperature: npc.temperature }
    );

    res.json({ reply: result.choices[0].message.content });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Streaming Responses

For real-time streaming responses:

async function chatStreamWithAI(playerToken, gameId, messages, onChunk) {
  const response = await fetch(
    `${PLAYKIT_BASE_URL}/ai/${gameId}/v2/chat`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${playerToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        messages,
        model: 'gpt-4o-mini',
        stream: true
      })
    }
  );

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let fullContent = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    const lines = chunk.split('\n');

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.substring(6);
        if (data === '[DONE]') continue;

        try {
          const parsed = JSON.parse(data);
          const content = parsed.choices?.[0]?.delta?.content;
          if (content) {
            fullContent += content;
            onChunk(content);
          }
        } catch (e) {
          // Skip invalid JSON
        }
      }
    }
  }

  return fullContent;
}

// SSE endpoint example
app.get('/api/chat-stream', authMiddleware, async (req, res) => {
  const playerToken = req.headers.authorization.substring(7);
  const { message } = req.query;

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  try {
    await chatStreamWithAI(
      playerToken,
      'your-game-id',
      [{ role: 'user', content: message }],
      (chunk) => {
        res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
      }
    );
    res.write('data: [DONE]\n\n');
    res.end();
  } catch (error) {
    res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
    res.end();
  }
});

Image Generation

async function generateImage(playerToken, gameId, prompt, options = {}) {
  const response = await fetch(
    `${PLAYKIT_BASE_URL}/ai/${gameId}/v2/image`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${playerToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        prompt,
        model: options.model || 'flux-1-schnell',
        size: options.size || '1024x1024',
        response_format: 'b64_json'
      })
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error?.message || 'Image generation failed');
  }

  const result = await response.json();
  return result.data[0].b64_json;
}

Using SDK on Backend

You can use the PlayKit SDK directly on your backend with server mode:

import { PlayKitSDK } from 'playkit-sdk';

async function handleUserRequest(playerToken, userMessage) {
  const sdk = new PlayKitSDK({
    gameId: 'your-game-id',
    playerToken: playerToken,  // Pass token directly
    mode: 'server'             // Disable UI features
  });

  await sdk.initialize();

  // Make AI calls - costs charged to the user
  const chat = sdk.createChatClient('gpt-4o-mini');
  const response = await chat.chat(userMessage);

  return response.content;
}

For token validation without SDK instantiation, use TokenValidator:

import { TokenValidator } from 'playkit-sdk';

const validator = new TokenValidator();

// Express middleware using TokenValidator
async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.substring(7);
  if (!token) {
    return res.status(401).json({ error: 'Missing token' });
  }

  try {
    req.user = await validator.validateToken(token, 'your-game-id');
    req.playerToken = token;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Creating SDK instances per request has overhead. For high-traffic scenarios, prefer direct API calls with token validation.

Security Best Practices

1. Never Trust Client-Provided User Info

Always validate tokens server-side:

// ❌ Wrong: trusting client-provided userId
app.post('/api/save-progress', (req, res) => {
  const { userId, progress } = req.body;
  saveProgress(userId, progress);  // Anyone can impersonate!
});

// ✅ Correct: extract userId from validated token
app.post('/api/save-progress', authMiddleware, (req, res) => {
  const userId = req.user.userId;  // From validated token
  const { progress } = req.body;
  saveProgress(userId, progress);
});

2. Check Balance Before Expensive Operations

app.post('/api/generate-art', authMiddleware, async (req, res) => {
  const { balance } = req.user;

  // Estimate cost (image generation is expensive)
  const estimatedCost = 0.05; // $0.05 per image

  if (balance < estimatedCost) {
    return res.status(402).json({
      error: 'Insufficient balance',
      required: estimatedCost,
      current: balance
    });
  }

  // Proceed with generation...
});

3. Handle Token Expiration

async function makeAICall(token, ...args) {
  try {
    return await chatWithAI(token, ...args);
  } catch (error) {
    if (error.message.includes('AUTH_INVALID_TOKEN')) {
      // Token expired - client needs to re-authenticate
      throw new TokenExpiredError('Please re-authenticate');
    }
    throw error;
  }
}

4. Rate Limiting

Implement rate limiting to prevent abuse:

import rateLimit from 'express-rate-limit';

const aiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 20, // 20 requests per minute per user
  keyGenerator: (req) => req.user?.userId || req.ip,
  message: { error: 'Too many requests, please slow down' }
});

app.use('/api/ai/*', authMiddleware, aiLimiter);

Error Handling

Common error codes from PlayKit API:

CodeDescriptionAction
AUTH_MISSING_HEADERNo Authorization headerReturn 401, prompt login
AUTH_INVALID_TOKENToken invalid or expiredReturn 401, prompt re-login
AUTH_WRONG_TOKEN_TYPEWrong token type for endpointCheck token type
PLAYER_INSUFFICIENT_CREDITNot enough balanceReturn 402, prompt recharge
GAME_NOT_FOUNDInvalid game IDCheck configuration
GAME_SUSPENDEDGame is suspendedContact support
async function handlePlayKitError(error, res) {
  const errorMap = {
    'AUTH_INVALID_TOKEN': { status: 401, message: 'Please re-authenticate' },
    'PLAYER_INSUFFICIENT_CREDIT': { status: 402, message: 'Insufficient balance' },
    'GAME_NOT_FOUND': { status: 400, message: 'Invalid game configuration' },
  };

  const mapped = errorMap[error.code] || { status: 500, message: 'Server error' };
  return res.status(mapped.status).json({ error: mapped.message, code: error.code });
}

Complete Example

Here's a complete Express.js backend setup:

import express from 'express';
import cors from 'cors';

const app = express();
app.use(cors());
app.use(express.json());

const PLAYKIT_BASE_URL = 'https://playkit.ai';
const GAME_ID = process.env.GAME_ID;

// Validation middleware
async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.substring(7);
  if (!token) {
    return res.status(401).json({ error: 'Missing token' });
  }

  try {
    const response = await fetch(
      `${PLAYKIT_BASE_URL}/api/external/player-info`,
      {
        headers: {
          'Authorization': `Bearer ${token}`,
          'X-Game-Id': GAME_ID
        }
      }
    );

    if (!response.ok) throw new Error('Invalid token');

    req.user = await response.json();
    req.playerToken = token;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Authentication failed' });
  }
}

// Chat endpoint
app.post('/api/chat', authMiddleware, async (req, res) => {
  const { message, systemPrompt } = req.body;

  try {
    const response = await fetch(
      `${PLAYKIT_BASE_URL}/ai/${GAME_ID}/v2/chat`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${req.playerToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          messages: [
            { role: 'system', content: systemPrompt || 'You are a helpful assistant.' },
            { role: 'user', content: message }
          ],
          model: 'gpt-4o-mini',
          stream: false
        })
      }
    );

    const result = await response.json();

    if (!response.ok) {
      throw { code: result.error?.code, message: result.error?.message };
    }

    res.json({
      reply: result.choices[0].message.content,
      usage: result.usage
    });
  } catch (error) {
    console.error('Chat error:', error);
    res.status(500).json({ error: error.message || 'Chat failed' });
  }
});

// Get user info endpoint
app.get('/api/me', authMiddleware, (req, res) => {
  res.json({
    userId: req.user.userId,
    nickname: req.user.nickname,
    balance: req.user.balance
  });
});

app.listen(3000, () => {
  console.log('Backend server running on port 3000');
});

Next Steps