Security
Verify Phoenix signatures and protect wallet operations
Every Phoenix Prediction wallet operation is signed. Your backend must verify the signature before changing a player balance.
Verify before balance changes
Treat unsigned or invalidly signed wallet operations as untrusted. They should not reach wallet business logic.
Platform Public Key
Wallet webhooks are signed with the single platform key. Fetch it as SPKI PEM:
GET https://prediction.phoenixverse.io/.well-known/signing-key.pemThis flat route serves one Ed25519 public key for the whole runtime, and every wallet operation Phoenix sends is signed with its private half. Verify wallet signatures against this key only.
Do not confuse it with your per-operator key at /o/{operator_code}/.well-known/signing-key.pem. That one is what Phoenix uses to verify your launch JWTs and Operator API calls (see Operator Launch Keys), not the other way around.
Cache the platform key, and refetch it if signature verification fails after a Phoenix key rotation notice.
Request Headers
Every wallet request carries these lowercase headers:
| Header | Description |
|---|---|
content-type: application/json | JSON request body |
x-provider: phoenix-prediction-market | Provider identifier |
x-request-id | UUIDv7 tracing identifier for the HTTP attempt |
signature | Unpadded base64url Ed25519 signature over the raw request body |
Money moves carry one additional header:
| Header | Description |
|---|---|
idempotency-key | Platform-generated dedup key for the move |
idempotency-key is present on POST /wallet/transactions and on POST /wallet/transactions/status (the status probe reuses the original move envelope, so it carries the same key). It is absent on POST /wallet/balance, which never moves money.
Verify the signature over the exact raw body bytes. Do not re-serialize JSON before verifying.
Node.js Verification Example
The signature header is unpadded base64url Ed25519 over the exact raw body bytes, produced with the platform private key. Decode it and verify against the platform public PEM.
import { createPublicKey, verify } from 'node:crypto';
const publicKey = createPublicKey(process.env.PHOENIX_PREDICTION_PUBLIC_KEY_PEM!);
export function verifyPhoenixSignature(rawBody: Buffer, signature: string) {
const decoded = Buffer.from(signature, 'base64url');
return verify(null, rawBody, publicKey, decoded);
}For Ed25519, Node's verify algorithm argument is null. Buffer.from(signature, 'base64url') decodes the unpadded signature correctly, so you do not need to add padding.
Endpoint Protection
- Accept wallet calls only on HTTPS.
- Verify the signature before parsing business fields.
- Reject missing or invalid signatures with HTTP
401and a plain JSON body such as{ "error": "bad_signature" }. Phoenix treats400/401/403as terminal from the status code alone, so a recognizable string member is enough. Only a business rejection uses422with anapplication/problem+jsonbody carrying acodemember (see Wallet Operations). - Keep a durable transaction table keyed by operation and idempotency key.
- Log request IDs, wallet response type, and signature verification result.
- Do not rely on client-side Phoenix events as proof that money moved.
Environment Separation
Use separate allowlists and logging views for the sandbox and prod environments where your platform supports it. At minimum, every wallet operation log should include the Phoenix environment value so finance can reconcile against the correct ledger.
Response Evidence
For money-moving requests, store enough evidence to prove what both sides exchanged:
- Request body hash
- Request
signatureheader value - Response HTTP status
- Response body hash
- Operator wallet transaction ID
- Operator reservation ID, when the operation creates a reservation
- Processed timestamp
Operator Launch Keys
You also provide Phoenix with an Ed25519 public key. Phoenix uses that key to verify:
- Launch JWTs minted by your backend
- Signed requests you make to the Operator API
Your private key must stay server-side. Rotate it if it is exposed.