Draw service
Signed requests
Draw requests carry two credentials: your secret key, and a proof signed with a private key that never leaves your server.
A secret key is a password: whoever copies it can use it. A signed proof is different. It is made fresh for each request, it names the exact method and URL, it can be used once, and it expires within minutes. We store only your public key, so a leak on our side gives nobody the ability to sign.
The BitRaffle-Proof header
Three base64url segments joined by dots: header.payload.signature, the same shape as a JWT.
| Part | Content |
|---|---|
| Header | {"alg":"EdDSA","typ":"hwt"} |
| Payload | The claims below, as JSON. |
| Signature | Ed25519 over the ASCII bytes of header.payload. |
| Claim | Value |
|---|---|
sub | Your secret key's prefix: its first 11 characters, e.g. sk_4f3a9c1e. |
htu | The method and URL, without query string: POST https://www.bitraffle.io/api/v1/draws/commit. |
nonce | A random value, never reused. A repeated nonce is refused with 409. |
iat | Issued-at, in Unix seconds. |
exp | Expiry, in Unix seconds. At most 300 seconds after iat. |
We allow 60 seconds of clock difference either way. Keep your server's clock synchronised.
Sign in code
import { generateKeyPairSync, randomBytes, sign } from "node:crypto";
const b64url = (buf) => Buffer.from(buf).toString("base64url");
// Once: create a key pair. Keep the private key secret (a secrets manager,
// not your repo). Register publicKeyB64 with POST /draws/register-key.
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
const publicKeyB64 = b64url(publicKey.export({ format: "der", type: "spki" }));
// The first 11 characters of your secret key, e.g. "sk_4f3a9c1e".
const KEY_PREFIX = process.env.BITRAFFLE_SECRET_KEY.slice(0, 11);
// Per request: a fresh proof bound to this method and URL.
function proof({ method, url, ttlSec = 60 }) {
const u = new URL(url);
const iat = Math.floor(Date.now() / 1000);
const header = b64url(JSON.stringify({ alg: "EdDSA", typ: "hwt" }));
const payload = b64url(JSON.stringify({
sub: KEY_PREFIX,
htu: `${method.toUpperCase()} ${u.protocol}//${u.host}${u.pathname}`,
nonce: randomBytes(16).toString("base64url"),
iat,
exp: iat + ttlSec,
}));
const signature = sign(null, Buffer.from(`${header}.${payload}`), privateKey);
return `${header}.${payload}.${b64url(signature)}`;
}import base64, json, os, time
from urllib.parse import urlsplit
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
def b64url(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
# Once: create a key pair. Keep the private key secret (a secrets manager,
# not your repo). Register public_key_b64 with POST /draws/register-key.
private_key = Ed25519PrivateKey.generate()
public_key_b64 = b64url(private_key.public_key().public_bytes(
serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo))
# The first 11 characters of your secret key, e.g. "sk_4f3a9c1e".
KEY_PREFIX = os.environ["BITRAFFLE_SECRET_KEY"][:11]
# Per request: a fresh proof bound to this method and URL.
def proof(method: str, url: str, ttl: int = 60) -> str:
u = urlsplit(url)
iat = int(time.time())
header = b64url(json.dumps({"alg": "EdDSA", "typ": "hwt"}).encode())
payload = b64url(json.dumps({
"sub": KEY_PREFIX,
"htu": f"{method.upper()} {u.scheme}://{u.netloc}{u.path}",
"nonce": b64url(os.urandom(16)),
"iat": iat,
"exp": iat + ttl,
}).encode())
signature = private_key.sign(f"{header}.{payload}".encode())
return f"{header}.{payload}.{b64url(signature)}"The Python version uses the cryptography package (pip install cryptography).
Register your public key
Once per secret key. This is the only draw call that needs no proof: you are registering the key proofs will be checked against.
curl -X POST https://www.bitraffle.io/api/v1/draws/register-key \
-H "Authorization: Bearer $BITRAFFLE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "publicKey": "MCowBQYDK2VwAyEA…" }'await fetch("https://www.bitraffle.io/api/v1/draws/register-key", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BITRAFFLE_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ publicKey: publicKeyB64 }),
});To replace it later, call POST /draws/rotate-key with the new public key. That request must be signed by the current key, so a stolen secret key alone cannot take over your draws.
When a proof is refused
| Status | error | Usual cause |
|---|---|---|
| 401 | missing_proof | No BitRaffle-Proof header. |
| 401 | no_agent_key | No public key registered for this secret key yet. |
| 401 | invalid_proof | The message says why: wrong htu (check scheme, host and path), expired, lifetime over 300 seconds, sub not matching the key, or a bad signature. |
| 409 | invalid_proof | This proof was already used. Sign a new one for every request, including retries. |