Neocortex
IntegrationsWeb SDK

Proxy Options

Every option of createNeocortexHandler, the routes it serves and the errors it returns

createNeocortexHandler(options) from @neocortex-link/web/server returns a standard (Request) => Promise<Response> handler. It throws at startup if apiKey, authorize or characters is missing.

import { createNeocortexHandler } from "@neocortex-link/web/server";

const handler = createNeocortexHandler({
  apiKey: process.env.NEOCORTEX_API_KEY!,
  characters: ["your-character-id"],
  authorize: async (request) => {
    const user = await getUser(request);
    return user ? { id: user.id } : null;
  },
  buildContext: async (user) => ({
    events: [{ priority: 1, content: `${user.id} has ${await coins(user.id)} coins` }],
  }),
});

Options

OptionDefaultDescription
apiKeyrequiredYour secret team key. Read it from the environment. It never leaves the server.
authorize(request)requiredIdentifies the visitor with your own auth. Return { id }, or null to refuse with 401. The id becomes the Neocortex playerId.
charactersrequiredThe characters the browser may talk to, as an array of ids or (user) => ids. Anything else is refused with 403.
questionSetsany of the team's setsThe question sets the browser may start quizzes with, as an array or (user) => ids.
basePath/api/neocortexWhere the handler is mounted. Use "" when it owns the whole host.
baseUrlhttps://api.neocortex.link/v3The Neocortex API. Point it at a local or staging API.
routessee RoutesTurns individual routes on or off, for example { transcribe: false }.
limitssee LimitsOverrides individual limits.
rateLimit30 requests a minute per user, in memory{ limit, windowSeconds } for the built-in limiter, a RateLimiter backed by a shared store, or false to turn it off.
speechRepliesOnlytrueOnly voice lines a character actually said to this user. false lets the browser voice any text in a character's voice.
allowClientContextfalseAccept scene metadata and events sent from the browser.
buildContext(user, turn)Adds trusted scene context to every chat turn. turn is { characterIds, message, request }. Return { metadata?, events? }.
filterText(text, context)Rewrites what characters say before it reaches the browser, for example for moderation. context is { user, route }, where route is "chat" or "quiz".
corsnone{ origins, allowHeaders? }. Only needed when the page is on another origin. allowHeaders adds request headers to the default content-type and authorization.
secretderived from apiKeySigns session and speech tokens. Set it to keep tokens valid across key rotations.
timeoutMs60000Milliseconds to wait for Neocortex to start answering.
fetchglobalThis.fetchA custom fetch for upstream calls.
onError(error, context)logs with console.errorCalled for failures worth logging. context is { route?, userId?, status }. Never receives the API key.

Routes

Each route is a path under basePath that maps to one Neocortex endpoint.

RouteProxy pathMethodNeocortex endpointDefault
chat/chatPOSTPOST /chaton
history/chat/sessionPOSTPOST /chat/sessionon
speech/audio/generatePOSTPOST /audio/generateon
speechStream/audio/streamPOSTPOST /audio/streamon
transcribe/audio/transcribePOSTPOST /audio/transcribeon
quiz/quizPOSTPOST /quizon
quizSession/quiz/sessionGETGET /quiz/sessionon
usage/usageGETGET /usageon
characters/charactersGETGET /charactersoff
questionSets/question-setsGETGET /question-setsoff
account/accountGETGET /accountoff

characters, questionSets and account are off by default because they list your whole team's content, and account includes your team's email address. When on, characters and questionSets are filtered by your allowlists.

With the default basePath, the chat route is POST /api/neocortex/chat. OPTIONS requests to any path return 204, with CORS headers for origins in cors.origins.

Limits

LimitDefaultDescription
messageChars500Longest player message, in characters. Also applies to quiz answers.
speechChars2000Longest text for speech synthesis, in characters. The API's own cap is 2000.
jsonBytes65536 (64 KB)Largest JSON body, in bytes.
audioBytes10485760 (10 MB)Largest audio upload for transcription, in bytes. The API's own cap is 10 MB.
charactersPerTurn6Most characters in one group turn. maxTurns is clamped to this too.
metadataItems40Most scene entities a turn may carry. Extra items are cut from the end, after the server's own.
eventItems20Most events a turn may carry. The oldest are cut first, so the server's own events are kept.
createNeocortexHandler({
  // ...
  limits: { messageChars: 300, charactersPerTurn: 3 },
  rateLimit: { limit: 60, windowSeconds: 60 },
});

Errors

Every refusal is JSON in the form { "error": "...", "code": "..." }. In the browser it arrives as a NeocortexError with the same status and code.

These are the codes the proxy returns itself:

StatusCodeWhen
400invalid_jsonThe body is not valid JSON, or not a JSON object.
400invalid_formThe transcription upload could not be read.
401unauthorizedauthorize returned no user, or threw.
403origin_not_allowedThe request came from another site or a sibling subdomain, and its origin is not in cors.origins.
403character_not_allowedThe character is not on your characters allowlist.
403question_set_not_allowedThe question set is not on your questionSets allowlist.
403speech_not_allowedThe text to speak has no valid speech token for this user and character.
404not_foundUnknown path, or a route that is turned off.
404session_not_foundThe session token is malformed, belongs to another user, or was signed with another secret (history, quiz turns and quiz state).
405method_not_allowedWrong method for the route. The Allow header names the right one.
409operation_in_progressA turn is already running for this conversation, or a quiz start for this user. Comes with Retry-After: 2.
413payload_too_largeThe body or the recording is larger than jsonBytes or audioBytes.
415unsupported_media_typeA JSON route was called without Content-Type: application/json, or transcription without multipart/form-data.
422invalid_charactercharacterId is missing or not a valid id.
422invalid_charactersA chat turn has no character ids, or more than charactersPerTurn.
422invalid_fieldA field has the wrong type, is empty when required, or speakerCharacterId is not one of characterIds.
422too_longA message, speech text or other text field is over its limit.
422missing_idsA quiz was started without a questionSetId.
429rate_limitedThe user is over the rate limit. Comes with Retry-After in seconds.
500internal_errorSomething unexpected failed inside the proxy, for example in your buildContext. Details go to onError.
502proxy_misconfiguredNeocortex refused the API key, or the key was deactivated or its workspace suspended. Details go to onError.
502upstream_errorNeocortex returned a server error. Details go to onError.
502upstream_unreachableNeocortex could not be reached, or did not start answering within timeoutMs.

Errors from Neocortex itself with status 400, 402, 403, 404, 409, 413, 422 or 429 are passed through with their message, their code (upstream_error when Neocortex sent none) and any Retry-After header, for example insufficient_credits or the quiz codes listed in Quiz. A finished quiz's closing turn is passed through too, with its session token and speech tokens signed. Every other upstream status becomes 502 upstream_error.

On this page