BitRaffleDocs

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.

PartContent
Header{"alg":"EdDSA","typ":"hwt"}
PayloadThe claims below, as JSON.
SignatureEd25519 over the ASCII bytes of header.payload.
ClaimValue
subYour secret key's prefix: its first 11 characters, e.g. sk_4f3a9c1e.
htuThe method and URL, without query string: POST https://www.bitraffle.io/api/v1/draws/commit.
nonceA random value, never reused. A repeated nonce is refused with 409.
iatIssued-at, in Unix seconds.
expExpiry, 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

StatuserrorUsual cause
401missing_proofNo BitRaffle-Proof header.
401no_agent_keyNo public key registered for this secret key yet.
401invalid_proofThe message says why: wrong htu (check scheme, host and path), expired, lifetime over 300 seconds, sub not matching the key, or a bad signature.
409invalid_proofThis proof was already used. Sign a new one for every request, including retries.