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 route serves one Ed25519 public key (content type application/x-pem-file), and every wallet call Phoenix sends, for sandbox and production alike, is signed with its private half. Verify wallet signatures against this key only.
Do not confuse it with your own keys: Phoenix verifies your launch tokens and Operator API calls with the public key or keys you registered for each environment (see Your Keys). Phoenix publishes no endpoint for those.
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, on production calls and wallet test calls alike. 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, on a public hostname. Phoenix does not call
localhost, private or internal addresses, or plainhttpin production (see Wallet base URL). - 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 records the body'scodeorerroras the reason. A refused reservation rejects the order; a capture, release or credit is sent again until it is accepted (see How Phoenix reads an answer), so a signature problem on those must be fixed quickly. - 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
Your Keys
You also provide Phoenix with an Ed25519 public key for each environment (two while you rotate). Phoenix uses it to verify:
- Launch JWTs minted by your backend, which may name the key in a
kidheader (Launch Tokens) - Signed requests you make to the Operator API (Request Signing)
Your private keys must stay server-side. To rotate one without a gap, ask Phoenix to register your next public key beside the current one: either key then verifies, you move your signers over, and Phoenix retires the old key by its id (see Rotating a Key). Entering a new public key under Manage > Your operator (your Admin, or Phoenix) is a hard cut instead: tokens and requests signed with the old key are refused from that moment, so switch your signing to the new key at the same time. Use that when a key has leaked.