Phoenix Prediction Docs

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.pem

This 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:

HeaderDescription
content-type: application/jsonJSON request body
x-provider: phoenix-prediction-marketProvider identifier
x-request-idUUIDv7 tracing identifier for the HTTP attempt
signatureUnpadded base64url Ed25519 signature over the raw request body

Money moves carry one additional header:

HeaderDescription
idempotency-keyPlatform-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 401 and a plain JSON body such as { "error": "bad_signature" }. Phoenix treats 400/401/403 as terminal from the status code alone, so a recognizable string member is enough. Only a business rejection uses 422 with an application/problem+json body carrying a code member (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 signature header 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.