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:
| Scheme | How it is sent | Who 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 cookie | The web app dashboards and the admin console |
| Signed Ed25519 challenge | Fields in the request body, no header | POST /v1/validators and PUT /v1/validators/payout-wallet |
| None | Anonymous | Browse, 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.
- Book a lease and pay it (see booking). The lease activates once the settlement worker confirms the on-chain payment.
- 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
404with problem typecredentials-already-shown. - Calling before activation returns
422lease-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)
| Endpoint | Domain prefix |
|---|---|
POST /v1/validators | swqos-validator-register-v2\0 |
PUT /v1/validators/payout-wallet | swqos-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:
| Field | Format |
|---|---|
pubkey | Validator identity pubkey, base58 |
payout_wallet | Payout wallet pubkey, base58 |
nonce_hex | 16 random bytes, 32 hex chars |
timestamp | Unix seconds, signed value |
signature_hex | 64-byte Ed25519 signature, 128 hex chars |
Verification errors:
| Status | Problem type | Cause |
|---|---|---|
400 | register-timestamp-skew | timestamp outside a 5 minute window around server time |
401 | register-signature-invalid | Signature does not verify over the rebuilt payload |
409 | register-nonce-replay | Nonce 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/healthGET /v1/openapi.json(the live generated spec)GET /v1/stats/marketplace-healthGET /v1/stats/marketplace-historyGET /v1/stats/leader-scheduleGET /v1/validators(browse, with filters)GET /v1/validators/{id}GET /v1/validators/{id}/leader-scheduleGET /v1/validators/stake-infoGET /v1/solana-pay/{memo}andPOST /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:
| Route | Limit | Keyed by |
|---|---|---|
POST /v1/leases/{id}/token | 12/min | Authenticated identity |
POST /v1/leases | 10/min | Trader |
GET /v1/validators | 600/min | Client IP |
| Everything else | 60/min | Identity (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/leasesPOST /v1/leases/{id}/extendPOST /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:
| Situation | Result |
|---|---|
Retry after a 200/201 with the byte-identical body | The cached response is replayed verbatim (24h window) |
| Same key, different body | 422 idempotency-key-reused |
| Concurrent request while the first is in flight | 409 idempotency-conflict with Retry-After: 1 |
| The original request failed (non-2xx) | The key is released, the retry re-runs the handler |
Authentication errors
| Status | Problem type | Meaning |
|---|---|---|
401 | auth-required | No valid credential on a protected route |
401 | user-not-provisioned | Session valid but no user row could be resolved |
403 | forbidden | Authenticated but not allowed (wrong role or not the owner) |
All errors are RFC 7807 problem documents. See the errors reference for the full catalogue.