Search docs

Find a documentation page

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

EnvironmentBase URL
Productionhttps://api.swqos.dev
Staginghttps://staging.api.swqos.dev
Local devhttp://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"
  }
}
  • data carries the endpoint-specific payload.
  • meta.request_id is 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/leases
  • POST /v1/leases/{id}/extend
  • POST /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:

SituationResult
Header missing400 idempotency-key-missing
Value is not a 26-char ULID400 idempotency-key-malformed
Retry within 24h, byte-identical bodyThe cached 200/201 response is replayed verbatim
Same key, different body422 idempotency-key-reused
Concurrent request while the first is in flight409 idempotency-conflict with Retry-After: 1
Original request returned a non-2xxKey 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 classLimitKeyed by
POST /v1/leases/{id}/token12 / minAuthenticated identity (API key)
POST /v1/leases10 / minTrader identity
GET /v1/validators600 / minClient IP
Everything else60 / minIdentity, 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 pathNotes
GET /v1/healthLiveness check, rate-limit exempt
GET /v1/openapi.jsonThe live generated OpenAPI 3.1 spec, rate-limit exempt
GET /v1/stats/marketplace-healthMarketplace-wide health stats
GET /v1/stats/marketplace-historyHistorical stats, ?range parameter
GET /v1/stats/leader-scheduleCluster leader-schedule overview
GET /v1/validatorsBrowse 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-schedulePer-validator leader schedule
GET /v1/validators/stake-infoPre-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 pathIdempotency-KeyNotes
POST /v1/leasesRequiredBook a lease, returns payment instructions, 10/min
GET /v1/leasesList your leases, newest first
GET /v1/leases/{id}Lease detail and status
POST /v1/leases/{id}/extendRequiredExtend in place at the frozen booking price
PATCH /v1/leases/{id}/auto-renewToggle auto-renew
POST /v1/leases/{id}/tokenMint a short-lived ES256 signing JWT, 12/min
POST /v1/leases/{id}/credentialsOne-shot reveal of TEE endpoint, API token, and TLS CA
POST /v1/leases/{id}/api-keysRotate the lease API token
POST /v1/leases/{id}/ratingRate a completed lease, 1 to 5 stars
GET /v1/paymentsSettled 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 pathAuthNotes
POST /v1/validatorsSigned Ed25519 challengeRegister, returns raw {validator_id, api_key}
PUT /v1/validators/payout-walletSigned Ed25519 challengeApplies to future bookings only
GET /v1/validators/meBearerYour listings
GET /v1/validators/{id}/dashboardBearer (owner)Operator dashboard data
GET /v1/validators/{id}/historyBearer (owner)Owner-facing time series
PATCH /v1/validators/{id}Bearer (owner)Update price, bio, endpoint
POST /v1/leases/{id}/usageBearer (lease JWT)Host-proxy sign-count ingest, idempotent per (lease, bucket) GREATEST upsert
POST /v1/validators/onboarding-sessionsBearerMint a one-time web-funnel onboarding token
GET /v1/validators/onboarding-sessions/{token}BearerPoll the funnel handoff status
POST /v1/validators/{id}/leases/{lease_id}/revokeBearer (owner) + Idempotency-KeyValidator-initiated revoke and refund
POST /v1/ratings/{rating_id}/replyBearer (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