Search docs

Find a documentation page

Authentication

API keys, web sessions, signed Ed25519 challenges, rate limits, and the public endpoints of the 2QUIC Marketplace API.

The 2QUIC Marketplace API authenticates requests four different ways depending on the endpoint:

SchemeHow it is sentWho uses it
API key (api_token)Authorization: Bearer <token>Trader SDKs and scripts, validator owner endpoints
Web session (clerk_session)Authorization: Bearer <Clerk session JWT> or the __session cookieThe web app dashboards and the admin console
Signed Ed25519 challengeFields in the request body, no headerPOST /v1/validators and PUT /v1/validators/payout-wallet
NoneAnonymousBrowse, stats, health, and Solana Pay endpoints

API keys authenticate you to the marketplace. They are not the short-lived ES256 lease JWT that POST /v1/leases/{id}/token mints, which authenticates your SDK to the validator's TEE. See the trader endpoints for the token flow.

API key format

API keys are opaque 26-character ULIDs. Send them as a bearer token:

curl https://api.swqos.dev/v1/leases \
  -H "Authorization: Bearer 01JXAMPLE0000000000000000Z"

The server never stores the cleartext. Each key is persisted only as an Argon2id hash plus a short indexed lookup prefix, so a key that is lost cannot be recovered, only rotated. An API key always resolves to a regular member identity. Per-route ownership checks (your leases, your validator) decide what it can reach.

For local development against http://localhost:8080, debug builds also accept an X-DEV-IDENTITY header to impersonate a dev user. It is ignored in production builds.

Trader API keys

A trader API key exists only after a lease activates. There is no token page in the dashboard.

  1. Book a lease and pay it (see booking). The lease activates once the settlement worker confirms the on-chain payment.
  2. Call the one-shot credentials reveal:
curl -X POST https://api.swqos.dev/v1/leases/{lease_id}/credentials \
  -H "Authorization: Bearer <Clerk session JWT>"
{
  "data": {
    "tee_endpoint_url": "https://tee.validator.example:443",
    "api_token": "01JXAMPLE0000000000000000Z",
    "tls_ca_cert": "-----BEGIN CERTIFICATE-----\n..."
  },
  "meta": { "request_id": "01JX...", "version": "1.0" }
}

The cleartext api_token is only ever returned in this response. Store it securely.

  • A second call returns 404 with problem type credentials-already-shown.
  • Calling before activation returns 422 lease-not-active.

Because the key is born from the first reveal, the bootstrap for API-first traders is the web app: book the first lease signed in from a validator's detail page (see booking), or drive POST /v1/leases directly with a Clerk session JWT as the bearer token.

Rotation

If a trader key leaks or is lost, rotate it:

curl -X POST https://api.swqos.dev/v1/leases/{lease_id}/api-keys \
  -H "Authorization: Bearer <Clerk session JWT>"

Rotation revokes the current token and returns a fresh one in the same {tee_endpoint_url, api_token, tls_ca_cert} shape as the reveal. Two concurrent rotations race and the loser gets 409 concurrent-rotation.

Validator API keys

Validator keys are minted exactly once, by registration. The validator-cli register command signs the registration challenge with your identity keypair and POST /v1/validators answers with a raw, un-enveloped body:

{ "validator_id": "01JX...", "api_key": "01JXAMPLE0000000000000000Z" }

The CLI never prints the key to stdout in normal operation. It writes the key to a file with mode 0600, named <validator_id>.api-key, alongside your identity keypair. The key is shown once and cannot be re-fetched, so back that file up.

Use the key for owner endpoints such as GET /v1/validators/me, the dashboard and history reads, PATCH /v1/validators/{id}, usage ingest, lease revocation, and rating replies. See the validator endpoints and the onboarding guide.

Web sessions (Clerk)

The web app (the public validator browse pages, the per-validator booking flow, and the trader and validator dashboards) authenticates with Clerk. The backend accepts the Clerk RS256 session JWT either as Authorization: Bearer or via the __session cookie, verifies it against the Clerk JWKS, and auto-provisions a user row on first touch.

Admin endpoints accept web sessions only, never API keys. Staff tiers map from the Clerk org_role claim: admin gets the Admin role, moderator, support, and staff get the Moderator role. Validator takedown is Admin-only, the other admin operations accept either tier.

Signed-challenge endpoints (no API key)

Two endpoints authenticate with an Ed25519 signature instead of a bearer token, because they run before or independently of any API key: POST /v1/validators (registration, no key exists yet) and PUT /v1/validators/payout-wallet (proves current control of the identity key, not just possession of an old API key).

The client signs this byte buffer with the validator identity keypair:

DOMAIN || identity_pubkey (32 bytes) || payout_wallet (32 bytes)
       || nonce (16 bytes) || timestamp (8 bytes, big-endian seconds)
EndpointDomain prefix
POST /v1/validatorsswqos-validator-register-v2\0
PUT /v1/validators/payout-walletswqos-validator-set-payout-v1\0

The v2 registration challenge binds the payout wallet into the signed payload, so the wallet that receives your share cannot be swapped in transit. The request body carries the proof fields:

FieldFormat
pubkeyValidator identity pubkey, base58
payout_walletPayout wallet pubkey, base58
nonce_hex16 random bytes, 32 hex chars
timestampUnix seconds, signed value
signature_hex64-byte Ed25519 signature, 128 hex chars

Verification errors:

StatusProblem typeCause
400register-timestamp-skewtimestamp outside a 5 minute window around server time
401register-signature-invalidSignature does not verify over the rebuilt payload
409register-nonce-replayNonce already seen, replayed request

The signature is produced locally from the identity keypair file on the operator machine, never through the enclave (the enclave signer only accepts TLS 1.3 CertificateVerify payloads). validator-cli register and validator-cli set-payout-wallet build and sign the challenge for you, including a --dry-run mode that prints the signed body without sending it.

Public endpoints

These endpoints require no authentication:

  • GET /v1/health
  • GET /v1/openapi.json (the live generated spec)
  • GET /v1/stats/marketplace-health
  • GET /v1/stats/marketplace-history
  • GET /v1/stats/leader-schedule
  • GET /v1/validators (browse, with filters)
  • GET /v1/validators/{id}
  • GET /v1/validators/{id}/leader-schedule
  • GET /v1/validators/stake-info
  • GET /v1/solana-pay/{memo} and POST /v1/solana-pay/{memo}

The Solana Pay endpoints are public by design of the wallet protocol. The unguessable 26-character ULID memo in the URL is the capability that scopes them to one payment.

Rate limits

Limits are enforced fleet-wide with a Redis token bucket:

RouteLimitKeyed by
POST /v1/leases/{id}/token12/minAuthenticated identity
POST /v1/leases10/minTrader
GET /v1/validators600/minClient IP
Everything else60/minIdentity (IP when anonymous)

Limited routes always return X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. A denied request gets 429 with problem type rate-limit-exceeded and a Retry-After header. GET /v1/health and GET /v1/openapi.json are exempt. The limiter fails open if Redis is unavailable.

Idempotency

Exactly three endpoints require an Idempotency-Key header:

  • POST /v1/leases
  • POST /v1/leases/{id}/extend
  • POST /v1/validators/{id}/leases/{lease_id}/revoke

The key must be a 26-character Crockford base32 ULID. A missing or malformed key returns 400 idempotency-key-missing or idempotency-key-malformed. Keys are scoped per user, method, path, and key value. Behavior on retry:

SituationResult
Retry after a 200/201 with the byte-identical bodyThe cached response is replayed verbatim (24h window)
Same key, different body422 idempotency-key-reused
Concurrent request while the first is in flight409 idempotency-conflict with Retry-After: 1
The original request failed (non-2xx)The key is released, the retry re-runs the handler

Authentication errors

StatusProblem typeMeaning
401auth-requiredNo valid credential on a protected route
401user-not-provisionedSession valid but no user row could be resolved
403forbiddenAuthenticated but not allowed (wrong role or not the owner)

All errors are RFC 7807 problem documents. See the errors reference for the full catalogue.