Skip to main content

Authentication

PolTrading uses two authentication mechanisms depending on the endpoint.

Overview

EndpointAuth TypeDescription
GET /, GET /healthzNonePublic endpoints
GET /v1/markets, GET /v1/quotesGeo-gating onlyRead market data
POST /v1/ordersSigned payloadEnvelope checked at the edge (market ID, deadline, size); the base64-decoded OrderRequest payload is forwarded to PolCore POST /orders byte-exact
POST /internal/ingest/marketHMAC + Ed25519Feed ingestion

Credential Management

CredentialStorageRotationAccess
HMAC signing keyEnvironment variable (testnet; never committed)Manual, documented procedureFeed ingestion only
Ed25519 private keyEnvironment variable (testnet; never committed per AGENTS.md secrets rules)Per-deploymentFeed signature verification
mTLS client certificateFile system (restricted permissions)Automated via cert-managerOutpost ↔ PolCore tunnel (mutual: pinned CA authenticates the backend on the outbound leg; client cert authenticates the outpost on the backend's intake leg)
GeoIP2 databaseLocal file (read-only)Monthly updateEdge middleware

All credentials are zeroed in memory when the service is sealed via scuttle.

HMAC-SHA256 (Feed Ingestion)

Used for authenticating feed data from the AI Feeder pipeline.

Required Headers

HeaderFormatDescription
X-Outpost-TimestampUnix epoch secondsMust be within 300s of server time
X-Outpost-Nonce16-128 char hexUnique per request (replay protection)
X-Outpost-Signaturev1=<hex-hmac>HMAC-SHA256 signature

Signing Formula

signature = HMAC-SHA256(
key = OUTPOST_INGEST_HMAC,
message = timestamp + "." + nonce + "." + body
)

Python Example

import hashlib
import hmac
import os
import time

def sign_request(hmac_key: str, body: bytes) -> dict:
timestamp = str(int(time.time()))
nonce = os.urandom(16).hex()

# Compute HMAC-SHA256
mac = hmac.new(
bytes.fromhex(hmac_key),
f"{timestamp}.{nonce}.".encode() + body,
hashlib.sha256,
).hexdigest()

return {
"X-Outpost-Timestamp": timestamp,
"X-Outpost-Nonce": nonce,
"X-Outpost-Signature": f"v1={mac}",
}

# Usage with feed ingestion
body = json.dumps({"conditionId": "0x...", "d": "...", "s": "...", "k": "ed25519-sha256"})
headers = sign_request(HMAC_KEY, body.encode())
response = requests.post(
"https://outpost.poltrading.com/internal/ingest/market",
headers=headers,
data=body,
)

Go Example

func signRequest(hmacKey string, body []byte) map[string]string {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonce := make([]byte, 16)
rand.Read(nonce)

mac := hmac.New(sha256.New, []byte(hmacKey))
mac.Write([]byte(timestamp + "." + hex.EncodeToString(nonce) + "."))
mac.Write(body)
sig := "v1=" + hex.EncodeToString(mac.Sum(nil))

return map[string]string{
"X-Outpost-Timestamp": timestamp,
"X-Outpost-Nonce": hex.EncodeToString(nonce),
"X-Outpost-Signature": sig,
}
}

Verification Flow

  1. Parse X-Outpost-Timestamp as int64; reject if |server_time - ts| > 300
  2. Extract signature after v1= prefix
  3. Decode hex signature
  4. Compute expected HMAC; compare using constant-time hmac.Equal
  5. On success, claim nonce (dedup map, 600s TTL)

Ed25519 Content Signature

Used for signing market data content in signed envelopes.

Envelope Format

{
"d": "base64-encoded-document",
"s": "hex-encoded-ed25519-signature",
"k": "ed25519-sha256"
}

Signing (Producer)

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import hashes, serialization
import base64, hashlib, json

def sign_envelope(private_key: Ed25519PrivateKey, document: bytes) -> dict:
digest = hashlib.sha256(document).digest()
sig = private_key.sign(digest)

return {
"d": base64.b64encode(document).decode(),
"s": sig.hex(),
"k": "ed25519-sha256",
}

Verification (Consumer)

  1. Decode d from base64 to raw bytes
  2. Compute digest = SHA256(raw_bytes)
  3. Decode hex s (must be exactly 64 bytes)
  4. Verify with ed25519.Verify(publicKey, digest, signature)

Common Errors

ErrorCauseFix
401 unauthorizedBad HMAC signatureCheck key and signing formula
401 ingest_replayNonce already usedGenerate unique nonce per request
401 timestamp expiredTimestamp too old/newSync clock, check REPLAY_WINDOW_SECS
451 restricted_jurisdictionGeo-blocked countryUse VPN or different endpoint
429 too many requestsRate limit exceededBack off, check RPS_PER_IP