Introduction
Base URLs, response envelope, pagination, idempotency, rate limits, and the full endpoint inventory of the 2QUIC Marketplace API.
The 2QUIC Marketplace exposes a REST API for browsing validators, booking
SWQoS leases, paying them via Solana Pay, and minting the short-lived signing
JWTs your SDK presents to the TEE. The contract is an OpenAPI 3.1 document
generated directly from the handler code via utoipa. It is committed at
docs/openapi.json in the repository and served live by the API itself:
curl https://api.swqos.dev/v1/openapi.json
A CI drift gate regenerates the spec on every build, so the document always matches the deployed handlers.
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://api.swqos.dev |
| Staging | https://staging.api.swqos.dev |
| Local dev | http://localhost:8080 |
The web app rewrites its own /api/* calls to the API. That rewrite is an
internal detail of the frontend deployment. External callers always talk to
the base URLs above directly.
Versioning
Every route lives under the /v1 path prefix (the path is /v1/..., not
/api/v1/...). Successful responses also carry the envelope version "1.0"
in meta.version.
Response envelope
Every successful JSON response wraps its payload in the same envelope:
{
"data": { },
"meta": {
"request_id": "01JXF8Z3V0Q2YT5M9W4R7K2D6E",
"version": "1.0"
}
}
datacarries the endpoint-specific payload.meta.request_idis a ULID unique to the request. Include it in support requests and bug reports.
Errors do not use the envelope. They are flat RFC 7807 Problem Details
documents served with Content-Type: application/problem+json, and the
type field is a stable URI ending in a slug you can switch on. The full
slug catalogue lives on the errors page.
One endpoint is deliberately not enveloped: POST /v1/validators
(validator registration) returns a raw {"validator_id": ..., "api_key": ...}
object for byte-for-byte compatibility with validator-cli.
Pagination
List endpoints use opaque cursors. The page and its cursor nest inside the
envelope's data:
{
"data": {
"data": [ ],
"next_cursor": "opaque-cursor-or-null"
},
"meta": {
"request_id": "01JXF8Z3V0Q2YT5M9W4R7K2D6E",
"version": "1.0"
}
}
Pass next_cursor back as the cursor query parameter to fetch the next
page. A null cursor means you reached the end. Limits are per endpoint:
GET /v1/validators accepts limit 1 to 50 (default 20), the admin audit
log accepts 1 to 200 (default 50). A bad value returns 400 invalid-limit,
and a cursor the server did not mint returns 400 invalid-cursor.
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. Generate a fresh one per logical operation and reuse the same one on retries:
curl -X POST https://api.swqos.dev/v1/leases \
-H "Authorization: Bearer $API_TOKEN" \
-H "Idempotency-Key: 01JXF8Z3V0Q2YT5M9W4R7K2D6E" \
-H "Content-Type: application/json" \
-d '{"validator_id": "...", "epoch_start": 820, "epoch_count": 5}'
Keys are scoped per user, method, path, and key, and the replay cache is Redis-backed. The exact semantics:
| Situation | Result |
|---|---|
| Header missing | 400 idempotency-key-missing |
| Value is not a 26-char ULID | 400 idempotency-key-malformed |
| Retry within 24h, byte-identical body | The cached 200/201 response is replayed verbatim |
| Same key, different body | 422 idempotency-key-reused |
| Concurrent request while the first is in flight | 409 idempotency-conflict with Retry-After: 1 |
| Original request returned a non-2xx | Key is released, the retry re-runs the handler |
Only 200/201 responses enter the 24-hour replay cache. Error responses
never get cached, so a failed booking can always be retried with the same key.
Rate limits
Limits are enforced fleet-wide with a Redis token bucket, per endpoint class:
| Endpoint class | Limit | Keyed by |
|---|---|---|
POST /v1/leases/{id}/token | 12 / min | Authenticated identity (API key) |
POST /v1/leases | 10 / min | Trader identity |
GET /v1/validators | 600 / min | Client IP |
| Everything else | 60 / min | Identity, or IP when unauthenticated |
Limited routes always return X-RateLimit-Limit, X-RateLimit-Remaining,
and X-RateLimit-Reset headers. On deny you get a
429 rate-limit-exceeded problem document plus a Retry-After header.
GET /v1/health and GET /v1/openapi.json are exempt.
The token endpoint's 12/min budget is generous on purpose: the SDK refreshes a JWT roughly once per 10 minutes, so even multi-process setups stay far below the limit. If you hit it, you are minting JWTs more often than you need to.
Endpoint inventory
The tables below mirror the live router. The machine-readable version of the
same inventory is the OpenAPI document at GET /v1/openapi.json.
Public (no authentication)
| Method and path | Notes |
|---|---|
GET /v1/health | Liveness check, rate-limit exempt |
GET /v1/openapi.json | The live generated OpenAPI 3.1 spec, rate-limit exempt |
GET /v1/stats/marketplace-health | Marketplace-wide health stats |
GET /v1/stats/marketplace-history | Historical stats, ?range parameter |
GET /v1/stats/leader-schedule | Cluster leader-schedule overview |
GET /v1/validators | Browse listings with filters and cursor pagination, 600/min/IP |
GET /v1/validators/{id} | Validator detail, public history folded in via ?range |
GET /v1/validators/{id}/leader-schedule | Per-validator leader schedule |
GET /v1/validators/stake-info | Pre-registration stake and tier lookup by ?pubkey= |
GET /v1/solana-pay/{memo} | Solana Pay label and icon for a payment memo |
POST /v1/solana-pay/{memo} | Returns the unsigned atomic 90/10 split transaction |
The Solana Pay endpoints take no bearer token. The unguessable ULID memo issued at booking is the capability.
Trader (bearer token: Clerk session or API token)
Full request and response contracts on the trader endpoints page.
| Method and path | Idempotency-Key | Notes |
|---|---|---|
POST /v1/leases | Required | Book a lease, returns payment instructions, 10/min |
GET /v1/leases | List your leases, newest first | |
GET /v1/leases/{id} | Lease detail and status | |
POST /v1/leases/{id}/extend | Required | Extend in place at the frozen booking price |
PATCH /v1/leases/{id}/auto-renew | Toggle auto-renew | |
POST /v1/leases/{id}/token | Mint a short-lived ES256 signing JWT, 12/min | |
POST /v1/leases/{id}/credentials | One-shot reveal of TEE endpoint, API token, and TLS CA | |
POST /v1/leases/{id}/api-keys | Rotate the lease API token | |
POST /v1/leases/{id}/rating | Rate a completed lease, 1 to 5 stars | |
GET /v1/payments | Settled payment ledger |
Validator
Full contracts on the validator endpoints page. Registration and payout-wallet changes authenticate with a signed Ed25519 challenge (nonce, timestamp, and signature over the request fields), not a bearer token. Everything else uses the bearer schemes above.
| Method and path | Auth | Notes |
|---|---|---|
POST /v1/validators | Signed Ed25519 challenge | Register, returns raw {validator_id, api_key} |
PUT /v1/validators/payout-wallet | Signed Ed25519 challenge | Applies to future bookings only |
GET /v1/validators/me | Bearer | Your listings |
GET /v1/validators/{id}/dashboard | Bearer (owner) | Operator dashboard data |
GET /v1/validators/{id}/history | Bearer (owner) | Owner-facing time series |
PATCH /v1/validators/{id} | Bearer (owner) | Update price, bio, endpoint |
POST /v1/leases/{id}/usage | Bearer (lease JWT) | Host-proxy sign-count ingest, idempotent per (lease, bucket) GREATEST upsert |
POST /v1/validators/onboarding-sessions | Bearer | Mint a one-time web-funnel onboarding token |
GET /v1/validators/onboarding-sessions/{token} | Bearer | Poll the funnel handoff status |
POST /v1/validators/{id}/leases/{lease_id}/revoke | Bearer (owner) + Idempotency-Key | Validator-initiated revoke and refund |
POST /v1/ratings/{rating_id}/reply | Bearer (owner) | One-time public reply to a rating |
Admin
Five staff-only routes exist under /v1/admin/ (validator suspend, validator
takedown, lease revoke, audit log, reputation recompute). They authenticate
with a Clerk session whose organization role grants staff access, and
takedown requires the Admin tier. They are operational tooling and are not
documented further here.
Next steps
The two bearer schemes, where tokens come from, and the signed-challenge flow for validator registration.
RFC 7807 problem documents and the full slug catalogue by status code.
Browse, book, pay, reveal credentials, and mint signing JWTs.
Register, manage your listing, ingest usage, and moderate leases.