Skip to content

Your own backend

You already have a product, a server, and users. You do not want a desktop shell or a marketplace listing; you want metered model access with your own users attributed individually. That is what an app principal is: a server-side identity keyed by an Ed25519 keypair you generate and hold, whose signature your backend uses to mint short-lived tokens for individual end users. Those users’ clients can then talk to the AI proxy directly. You may instead keep inference on your backend when your application needs server-side processing.

The shape is deliberately the same as a GitHub App: you hold a private key, a customer organization installs your app and grants it capabilities, and every call is attributed to the installation.

Thing Who holds it What it is
App you a slug registered under an organization, with a display name
Signing key you only an Ed25519 keypair. WAMP stores the public half and a kid derived from it; the private half never leaves your infrastructure
Installation a customer organization the grant of capabilities to your app in that organization. Identified by a UUID your backend keeps
End-user token minted per request-burst by your backend a short-lived JWT signed by your key, naming one of your users

Use Account Center → Apps in the browser: pick an organization, choose a slug and a name, then generate a keypair and register the public half.

POST /api/apps { orgId, slug, name }
POST /api/apps/:slug/keys { publicKeyPem } → { kid, ... }
DELETE /api/apps/:slug/keys/:kid

orgId is a required UUID on create, and a required query parameter when listing your apps — an app is always scoped to one organization. publicKeyPem must be an SPKI PEM (-----BEGIN PUBLIC KEY-----). The returned kid is the RFC 7638 JWK thumbprint of the key, which means you can compute it yourself and do not have to store what the server told you.

Generate the keypair in a trusted environment and register only its public half. Account Center can generate it locally in the browser. crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']) is the whole of it.

Your app is registered, but registration alone authorizes nothing. An organization has to install it, and the installation has to carry the AI capability — a valid signature on a token is not sufficient, because the proxy independently checks that the installation holds wamp.ai.invoke on the wamp-ai resource. An organization admin performs that grant.

For your own organization, install the app from the same Apps surface where you registered it. For a customer’s organization, the flow avoids asking them for a UUID: your backend opens an installation intent naming the capabilities you want, the customer’s admin reviews and authorizes it against their organization, and you poll for the result.

POST /api/apps/installation-intents { assertion, resourceAudience, capabilityIds }
POST /api/apps/installation-intents/:id/status { assertion }
GET /api/apps/installation-intents/:id (the admin's review screen)
POST /api/apps/installation-intents/:id/authorize { orgId } (the admin authorizes)

The assertion is a short-lived JWT signed by your app key — your credential on the intent routes, which take no human session. What you keep from the finished intent is the installation id; every token you mint afterwards names it.

This is the only cryptography you have to perform, and it is one JWT. Sign it with your app’s private key on every request-burst for a signed-in user of your product:

Claim Value
header alg EdDSA
header kid your key’s thumbprint
iss your app slug — the app is the issuer, not the platform
sub your own opaque id for the user. Matches ^(anon:)?[A-Za-z0-9._:-]{1,128}$, so hash it. Never PII, never a WAMP user id
aud wamp-proxy
installation_id the installation UUID from step 2
iat / exp issued-at and expiry

Signature verification is strict: EdDSA only, the kid must resolve to a non-revoked key of an app whose slug equals iss, the audience must be exactly wamp-proxy, a suspended app fails closed, and clock skew is tolerated to 30 seconds. Anything issued by the platform itself is rejected on this path — the issuer being your slug is what keeps end-user tokens out of the human-session code path.

The verifier requires integral iat and exp values with a positive lifetime of at most one hour. The reference client defaults to that maximum and rejects invalid ttlSeconds before signing. Treat the token as a bearer credential that reaches a browser and pick the shortest TTL your refresh story can carry.

import { createHash, createPrivateKey } from 'node:crypto';
import { SignJWT } from 'jose';
const key = createPrivateKey(process.env.WAMP_APP_KEY); // PKCS#8 PEM
// Call only after authenticating this user in your own application.
export async function tokenFor(userId) {
const now = Math.floor(Date.now() / 1000);
const subject = createHash('sha256').update(String(userId)).digest('hex');
return new SignJWT({ installation_id: process.env.WAMP_INSTALLATION_ID })
.setProtectedHeader({ alg: 'EdDSA', kid: process.env.WAMP_APP_KID })
.setIssuer('your-app-slug')
.setSubject(subject)
.setAudience('wamp-proxy')
.setIssuedAt(now)
.setExpirationTime(now + 900)
.sign(key);
}

4 · Call the proxy with the OpenAI-compatible HTTP API

Section titled “4 · Call the proxy with the OpenAI-compatible HTTP API”

The canonical AI surface is the OpenAI-compatible HTTP API under /v1. Send the end-user token as a bearer credential on every request. Use POST /v1/responses for new integrations; POST /v1/chat/completions is available for clients that require the Chat Completions contract.

First discover the models enabled on the deployment:

Terminal window
curl -sS https://api.vampikez.fun/v1/models \
-H "Authorization: Bearer $END_USER_TOKEN"

Then make a Responses request:

Terminal window
curl -sS https://api.vampikez.fun/v1/responses \
-H "Authorization: Bearer $END_USER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-5.6-terra",
"input": "Summarize this thread in three bullets.",
"max_output_tokens": 1024
}'

The response is an OpenAI response object. Read generated text from output_text, usage from usage, and check status before treating it as complete. stream: true returns named SSE events; a successful stream ends with response.completed, while response.incomplete and response.failed are terminal outcomes that your UI must distinguish.

If your client requires Chat Completions, send messages to /v1/chat/completions and read choices[0].message.content. Its stream ends with data: [DONE], unlike Responses streams.

Your product executes its own tools. Validate each returned function name and arguments, enforce your application’s authorization, and return a function_call_output for every call. On the next Responses request, replay the original input, the previous output items, and the tool results, keeping the tool definitions if another call is allowed. WAMP rejects previous_response_id and conversation; it does not retain your turn history.

Use the complete tool round-trip example and stream consumer. Those examples check HTTP and terminal response failures rather than returning an error body as if it were an answer.

The model catalog is deployment-specific. GET /v1/models returns available ids, aliases, context and output limits, tool support, input modalities, and pricing metadata. Cache that response and select only an advertised model.

Admitted inference attempts write usage records, including provider failures. Authentication and validation rejections are not model calls. For an app principal, a record carries your appId, the appInstallationId, the organization, the end-user subject, the model, token counts, cost, and latency — and no WAMP user, because there is not one involved.

The end-user subject comes from inside your signature, so it cannot be overridden: a payload user or safety_identifier that disagrees with a signed end-user token is rejected as an attribution conflict rather than quietly preferred. (A backend calling with an app-level credential rather than an end-user token does set user or safety_identifier in the payload, since in that case the app is the only thing asserting who its user was.)

You read it back per app, filtered by end user and time range:

GET /api/apps/:slug/usage?endUserId=&from=&to=&limit=
GET /api/apps/:slug/usage/summary

@wamp/app-sdk, and what to do until it ships

Section titled “@wamp/app-sdk, and what to do until it ships”

The SDK wraps all of the above. Its real export surface — verified against the source, since one name in circulation is out of date:

Export What it is
generateAppKeypair() returns { privateKeyPem, publicKeyPem, kid }
WampApp the app principal. issueUserToken({ endUserId, installationId, ttlSeconds? }), installationToken(installationId, resourceAudience?, opts?), createInstallationIntent(resourceAudience, capabilityIds), getInstallationIntent(id), buildAssertion()
WampCloud, WampCloudError the Cloud session client: sessions, turns, runs, artifacts, publications
END_USER_TOKEN_AUDIENCE, WAMP_CLOUD_RESOURCE_AUDIENCE the two audience strings, wamp-proxy and wamp-cloud

There is no appToken function; the backend-credential call is WampApp.installationToken(), whose second argument is the resource audience you want the token for.

// The shape once it is installable:
import { createHash } from 'node:crypto';
import { WampApp } from '@wamp/app-sdk';
const app = new WampApp({
slug: 'your-app-slug',
privateKeyPem: process.env.WAMP_APP_KEY,
});
const token = await app.issueUserToken({
endUserId: createHash('sha256').update('authenticated-user-id').digest('hex'),
installationId: process.env.WAMP_INSTALLATION_ID,
});

The end-user token from step 3 also authenticates the App document store at /a/<slug>/data. mine binds records to that token’s endUserId; shared binds them to the App. The server never accepts another user’s id from the request, and revoking the App key stops both model and data access.

WAMP does not mint an anonymous product session or hold your private signing key. Your backend owns user identity and token issuance. See Apps and credentials for the wider identity model.

An installation token issued for the wamp-cloud audience lets your backend drive durable agent sessions on WAMP’s own infrastructure — a long-running task started from your CRM, bot, or scheduler, rather than a streaming completion in a user’s client. That product has its own documentation at docs.cloud.vampikez.fun.