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:
- Frontend: User authenticates via PlayKit SDK, obtains a Player Token
- 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:
| Code | Description | Action |
|---|---|---|
AUTH_MISSING_HEADER | No Authorization header | Return 401, prompt login |
AUTH_INVALID_TOKEN | Token invalid or expired | Return 401, prompt re-login |
AUTH_WRONG_TOKEN_TYPE | Wrong token type for endpoint | Check token type |
PLAYER_INSUFFICIENT_CREDIT | Not enough balance | Return 402, prompt recharge |
GAME_NOT_FOUND | Invalid game ID | Check configuration |
GAME_SUSPENDED | Game is suspended | Contact 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
- Review Authentication for detailed auth flows
- See API Reference for complete API documentation
- Check Payment for balance management