Search docs

Find a documentation page

Validator endpoints

Public validator browsing, signed-challenge registration, owner dashboard and listing management, host-proxy usage ingest, lease revocation, and rating replies.

This page documents the validator side of the 2QUIC Marketplace API, plus the public browse and stats endpoints traders use to pick a validator. All shapes come from the generated OpenAPI contract, served live at GET https://api.swqos.dev/v1/openapi.json. The trader lease lifecycle lives on the Lease endpoints page.

Endpoint summary

MethodPathAuthNotes
GET/v1/validatorspublicbrowse with filters, cursor-paginated
GET/v1/validators/{id}publicdetail + availability + public history
GET/v1/validators/{id}/leader-schedulepublicleader slots, current epoch
GET/v1/validators/stake-infopublicpre-registration stake check
POST/v1/validatorssigned challengeregister, raw response
PUT/v1/validators/payout-walletsigned challengeraw response
POST/v1/validators/onboarding-sessionsbearerweb-funnel token mint
GET/v1/validators/onboarding-sessions/{token}bearerpoll funnel status
GET/v1/validators/mebearerowned validators
GET/v1/validators/{id}/dashboardbearer (owner)revenue, leases, security panel
GET/v1/validators/{id}/historybearer (owner)series including revenue
PATCH/v1/validators/{id}bearer (owner)price, bio, endpoint, shared config
POST/v1/leases/{id}/usagebearer (lease JWT)host-proxy sign-count ingest (always-on telemetry)
POST/v1/validators/{id}/leases/{lease_id}/revokebearer (owner) + Idempotency-Keyrevoke + prorated refund
POST/v1/ratings/{rating_id}/replybearer (owner)one public reply per rating

"Bearer" means Authorization: Bearer <token> with either a Clerk session JWT (web) or the validator API key returned at registration. The two signed-challenge endpoints take no bearer token at all: they authenticate with an Ed25519 signature from the validator identity key, described below. See Authentication for token sources and rate limits, and Errors for the problem-document format.

Success responses are wrapped in the standard envelope {"data": ..., "meta": {"request_id": "<ULID>", "version": "1.0"}}, with two exceptions: POST /v1/validators and PUT /v1/validators/payout-wallet return raw flat bodies so the validator-cli wire format stays byte-for-byte stable.

Browse validators

GET /v1/validators is anonymous and lists live validators only, ranked by reputation score and then stake. It carries the most generous rate limit in the API at 600 requests/min per IP.

curl "https://api.swqos.dev/v1/validators?region=us-east-1&available_now=true&limit=20"
Query paramTypeDefaultDescription
regionstringnoneExact AWS region match, e.g. us-east-1
price_min_lamportsu64noneMinimum exclusive price per epoch
price_max_lamportsu64noneMaximum exclusive price per epoch
available_nowbooleanfalseOnly validators free for the next 14-epoch window
cursorstringnoneOpaque cursor from a previous page
limitinteger20Page size, 1 to 50

The page nests inside the envelope's data:

{
  "data": {
    "data": [
      {
        "validator_id": "01JXEZV2P9R8S7T6W5X4Y3Z2A1",
        "identity_pubkey": "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2",
        "display_name": "Example Validator",
        "icon_url": null,
        "public_bio": "Frankfurt bare-metal, 24/7 ops.",
        "region": "us-east-1",
        "instance_tier": "standard",
        "status": "live",
        "current_stake_lamports": 1250000000000000,
        "uptime_30d_pct": 99.97,
        "reputation_score_0_100": 94.2,
        "price_per_epoch_lamports": 2000000000,
        "shared_enabled": true,
        "price_per_epoch_shared_lamports": 600000000,
        "max_concurrent_shared_leases": 4
      }
    ],
    "next_cursor": null
  },
  "meta": { "request_id": "01JXF0...", "version": "1.0" }
}

price_per_epoch_shared_lamports and max_concurrent_shared_leases are null unless shared_enabled is true. next_cursor is null on the last page. Errors: 400 invalid-limit, 400 invalid-price-bound, 400 invalid-cursor.

Validator detail

GET /v1/validators/{id} is anonymous and returns everything the booking form needs in one round-trip: listing data, performance metrics, the top 5 most recent ratings (with validator replies), epoch occupancy, and a public history series.

curl "https://api.swqos.dev/v1/validators/01JXEZV2P9R8S7T6W5X4Y3Z2A1?range=1w"

On top of the summary fields above, the detail adds:

FieldTypeDescription
website_urlstring or nullValidator-supplied website link
stake_percentilenumberPercent rank of stake across live validators, 0.0 to 1.0
slots_per_epochintegerSolana constant, 432000
leader_time_seconds_per_epochnumberExpected leader time per epoch from the stake fraction
latency_p50_ms / latency_p95_msnumber or null30-day sign-probe latency, null before first probe
recent_ratingsarrayTop 5 newest ratings: {rating_id, stars, comment, rated_at, validator_reply_text, validator_replied_at}
current_epochintegerCurrent Solana epoch from the API clock
next_available_epochintegerSee below
epoch_availabilityarraySparse occupancy, see below
historyobjectPublic history series, see below

Returns 404 validator-not-found for an unknown id.

Epoch availability (sparse occupancy)

epoch_availability covers the next 64 epochs but is sparse: only epochs covered by at least one active or pending_payment lease appear. An epoch absent from the array is fully free.

"epoch_availability": [
  { "epoch": 982, "exclusive_booked": true,  "shared_taken": 0 },
  { "epoch": 985, "exclusive_booked": false, "shared_taken": 2 }
]
FieldMeaning
epochAbsolute Solana epoch number
exclusive_bookedtrue when an exclusive lease covers this epoch (blocks both modes)
shared_takenCount of shared leases covering this epoch (a seat is free while below max_concurrent_shared_leases)

This is advisory display data for booking forms. Admission is enforced in Postgres at booking time, so a race between reading availability and posting a lease resolves to 409 lease-slot-taken or 409 shared-capacity-full, never a double booking.

next_available_epoch is the earliest epoch at or after current_epoch not covered by an active or pending_payment lease. It equals current_epoch when nothing is booked today, and otherwise points past the first contiguous run of bookings. Booking forms use it as the default starting epoch.

Public history

The history object is selected by the optional ?range= query param: 1d, 1w, 1m (default), or all. It contains range (echoed) and buckets, each bucket carrying t (RFC 3339 left edge), leases_activated, signs_count (real TEE signature volume), probe_success_count, probe_total_count, and nullable latency_p50_ms / latency_p95_ms. The public series carries no revenue field. Revenue appears only on the owner history endpoint below.

Leader schedule

GET /v1/validators/{id}/leader-schedule is anonymous and returns the validator's leader slots for the current epoch: epoch, absolute_slot, slot_index, slots_in_epoch, identity_pubkey, leader_slot_count, past_slot_count, share_pct, up to 12 upcoming_slots (absolute, ascending), and nullable next_leader_slot / next_leader_eta_seconds. Errors: 404 validator-not-found, 502 solana-rpc-unavailable.

Stake info (pre-registration check)

GET /v1/validators/stake-info?pubkey=<base58> is anonymous. The onboarding funnel and validator-cli use it to verify a pubkey is a known staked validator and to drive the tier recommendation before registering.

{
  "data": {
    "activated_stake_lamports": 1250000000000000,
    "total_network_stake_lamports": 380000000000000000,
    "stake_fraction": 0.0033,
    "is_known_validator": true
  },
  "meta": { "request_id": "01JXF0...", "version": "1.0" }
}

activated_stake_lamports is 0 and is_known_validator is false when the pubkey is absent from the current getVoteAccounts() set. Returns 503 solana-rpc-unavailable when the RPC is down.

Register a validator

POST /v1/validators is a public route authenticated by a signed Ed25519 challenge rather than a bearer token. The normal caller is validator-cli register, which builds and signs the challenge with the validator identity keypair file on the operator machine. The identity key is never used through the enclave for this, and never touches a browser. See Validator onboarding for the full funnel.

The signature covers the v2 challenge, concatenated in this exact order:

"swqos-validator-register-v2\0" || identity_pubkey[32] || payout_wallet[32] || nonce[16] || timestamp_be[8]

nonce is 16 CSPRNG bytes (hex-encoded as 32 chars in the body), tracked server-side to prevent replay. timestamp is Unix seconds, big-endian in the challenge, and must fall within plus or minus 5 minutes of server time.

curl -X POST https://api.swqos.dev/v1/validators \
  -H "Content-Type: application/json" \
  -d '{
    "pubkey": "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2",
    "payout_wallet": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
    "endpoint_url": "https://tee.example.com:443",
    "price_sol": 2.0,
    "bio": "Frankfurt bare-metal, 24/7 ops.",
    "nonce_hex": "9f86d081884c7d659a2feaa0c55ad015",
    "timestamp": 1781136000,
    "signature_hex": "<128 hex chars>",
    "region": "us-east-1",
    "tier": "standard",
    "shared_price_sol": 0.6,
    "max_concurrent_shared_leases": 4
  }'
FieldRequiredDescription
pubkeyyesValidator Ed25519 identity pubkey, base58 (32 bytes)
payout_walletyesBase58 wallet the 90% payment leg credits, signed into the challenge
endpoint_urlyesgRPC endpoint URL of the TEE host-proxy
price_solyesExclusive asking price per epoch in SOL, converted to lamports server-side
bioyesPublic listing bio
nonce_hexyes32 hex chars (16 bytes), anti-replay
timestampyesUnix seconds at signing time
signature_hexyes128 hex chars, Ed25519 signature over the challenge
onboard_tokennoOne-time token from the web funnel, attributes the listing to the operator's account
regionnoAWS region of the TEE, defaults to us-east-1
tiernoeconomy, standard (default), or performance
shared_price_solnoShared price per epoch in SOL, enables shared together with the seat cap
max_concurrent_shared_leasesnoShared seat cap, at least 1, required together with shared_price_sol

A bare CLI registration omits the shared pair and stays exclusive-only. Registration publishes the listing live immediately.

The 201 response is a raw flat object, not the standard envelope, for validator-cli compatibility:

{
  "validator_id": "01JXEZV2P9R8S7T6W5X4Y3Z2A1",
  "api_key": "01JXF0A1B2C3D4E5F6G7H8J9K0"
}

The api_key is returned exactly once. The server persists only an Argon2id hash. Store it securely: it is the bearer token for every owner endpoint below. The CLI prints it locally and it is never shown to the browser, even on the web funnel path.

Registration errors:

StatusSlugWhen
400register-timestamp-skewtimestamp outside the 5-minute window
400register-invalid-fieldMalformed pubkey, wallet, nonce, signature, or a negative/non-finite price
401register-signature-invalidEd25519 verification failed
409register-nonce-replayNonce already used
409validator-already-registeredPubkey already has a listing
422register-price-requiredprice_sol is 0 (a live listing needs a price)
422register-invalid-tierTier not in the enum
422register-invalid-regionUnknown region
422not-a-solana-validatorPubkey not in getVoteAccounts()
422invalid-onboarding-tokenFunnel token unknown, expired, or used
422register-shared-config-incompleteOnly one of the shared pair provided
422register-shared-price-invalidShared price not positive
422register-shared-seats-invalidSeat cap below 1
503solana-rpc-unavailableStake check could not run

Update the payout wallet

PUT /v1/validators/payout-wallet uses the same signed-challenge mechanism with a distinct domain tag, so a registration signature can never be replayed as a payout change:

"swqos-validator-set-payout-v1\0" || identity_pubkey[32] || payout_wallet[32] || nonce[16] || timestamp_be[8]

Body: {pubkey, payout_wallet, nonce_hex, timestamp, signature_hex}, the same field formats as registration. The usual caller is validator-cli set-payout-wallet. The 200 response is raw: {"payout_wallet": "<base58>"}.

The change applies to future bookings only. Each lease freezes the payout wallet at booking time, so payments for already-booked leases keep crediting the wallet that was set when the trader booked.

Errors: 400 register-timestamp-skew / register-invalid-field, 401 register-signature-invalid, 404 validator-not-found (no listing for that identity pubkey), 409 register-nonce-replay.

Onboarding sessions (web funnel)

The web funnel never sees the identity key. Instead it mints a one-time token, shows the operator a prefilled validator-cli register ... --onboard-token <token> command, and polls until the CLI completes registration. Both endpoints require a bearer token (in practice the operator's Clerk session in the web app).

POST /v1/validators/onboarding-sessions (no body) returns 201:

{
  "data": {
    "token": "01JXF1XAMPLE0NB0ARD1NGT0KEN",
    "expires_at": 1781137800
  },
  "meta": { "request_id": "01JXF0...", "version": "1.0" }
}

expires_at is Unix seconds. Sessions expire 30 minutes after creation.

GET /v1/validators/onboarding-sessions/{token} returns {"status": "pending", "validator_id": null} until the CLI registers with the token, then {"status": "completed", "validator_id": "<ULID>"}. An unknown token, or one belonging to a different account, returns 404 onboarding-session-not-found.

My validators

GET /v1/validators/me returns the caller's owned validators as a list (usually one entry). It is the only validator read that includes tee_endpoint_url, which never appears on public endpoints. Each item carries validator_id, identity_pubkey, region, status (draft, live, suspended, or takedown), price_per_epoch_lamports, shared_enabled, price_per_epoch_shared_lamports, max_concurrent_shared_leases, public_bio, and tee_endpoint_url.

Owner dashboard

GET /v1/validators/{id}/dashboard requires ownership and returns the data behind the /validator dashboard in one round-trip:

FieldDescription
revenue_lamports_30dGross lease totals activated in the trailing 30 days
net_revenue_lamports_30dSum of confirmed validator_share legs over 30 days, the net the payout wallet actually received
fee_bpsCurrent marketplace fee in basis points (1000 = 10%)
payout_wallet_addressWallet the 90% leg credits today
active_leasesActive leases: {lease_id, trader_pubkey, epoch_start, epoch_end, status, total_amount_lamports, signs_count}
signs_count_30dReal TEE signature volume served over 30 days
tee_healthhealthy when the listing is live, otherwise down
current_epochCurrent Solana epoch
recent_paymentsConfirmed settlements: gross amount, both split legs, confirmed_at, and the on-chain tx signature
securitySecurity-health panel, see note

trader_pubkey is the trader's account ULID, not a wallet address. Validators can deduplicate a trader across leases but never see personal information.

Two security panel fields are v1 placeholders: kms_policy_drift is always false until the IAM snapshot pipeline lands, and last_attestation_at currently echoes the registration timestamp. auth_failures_24h (failed probes in 24h) and cosign_verify_status (ok / stale / failed heuristic from probe recency) are live signals.

Errors: 403 forbidden when the caller does not own the validator, 404 validator-not-found.

Owner history

GET /v1/validators/{id}/history?range=1d|1w|1m|all (owner only, default 1m) returns the same bucket series as the public detail plus revenue_lamports per bucket: {t, revenue_lamports, leases_activated, signs_count, probe_success_count, probe_total_count, latency_p50_ms, latency_p95_ms}. Errors: 403 forbidden, 404 validator-not-found.

Update a listing

PATCH /v1/validators/{id} (owner only) updates only the fields present in the body and returns the refreshed public detail projection. All fields are optional, but an all-empty body is 422 patch-validator-empty.

curl -X PATCH https://api.swqos.dev/v1/validators/01JXEZV2P9R8S7T6W5X4Y3Z2A1 \
  -H "Authorization: Bearer $VALIDATOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "price_sol": 2.5, "shared_enabled": true, "shared_price_sol": 0.8, "max_concurrent_shared_leases": 4 }'
FieldConstraintsSemantics
price_sol> 0New exclusive price. Takes effect at the next epoch boundary, booked leases keep their frozen price
biostringReplaces the public bio
endpoint_urlstringNew TEE host-proxy URL
shared_enabledbooleanTurns the shared offering on or off
shared_price_sol> 0Shared price per epoch, independent of the exclusive price
max_concurrent_shared_leases>= 1Shared seat cap per epoch

Shared-offering semantics:

  • Setting shared_enabled: true requires a shared price and a seat cap, either in the same patch or already stored on the listing. Otherwise the call fails with 422 shared-config-incomplete.
  • Setting shared_enabled: false stops new shared bookings but keeps the stored price and cap, so re-enabling later is a one-field patch. Existing shared leases are unaffected.
  • Lowering max_concurrent_shared_leases never evicts existing leases. It only constrains new bookings, and it also changes the per-seat handshake quota stamped into future signing JWTs (each shared lease gets max(1, floor(budget / seat_cap)) of the handshake budget).

Other errors: 400 register-invalid-field (non-positive price or seat cap), 403 forbidden, 404 validator-not-found.

Usage ingest (host-proxy)

POST /v1/leases/{id}/usage is called by the validator's host-proxy, authenticated by that lease's own marketplace JWT (the same token minted by POST /v1/leases/{id}/token). Telemetry is always on and needs no validator-side configuration: the host-proxy keeps the freshest verified JWT per lease and pushes to the token's iss URL. It reports successful TEE signing calls in 1-minute buckets and feeds the signs_count metrics everywhere (dashboard, histories, rating eligibility).

The marketplace verifies its own signature (with a 15-minute expiry grace to cover the flush lag), binds the token to the lease (aud, val_pk, and lease_id must match the lease in the path), and clamps each signs_delta to the JWT quota.

{
  "reports": [
    {
      "bucket_start": "2026-06-11T12:34:00Z",
      "signs_delta": 41
    }
  ]
}
FieldConstraints
reports1024 max per batch. An empty batch returns 200 with accepted: 0
bucket_startRFC 3339, aligned to floor(now, 60s) by the reporter
signs_deltaSuccessful signing calls in the bucket, always positive (clamped to the JWT quota)

The lease rides in the URL path, not the body. The response is {"received": n, "accepted": n, "dropped": n}, where dropped covers future-dated buckets.

Inserts UPSERT with GREATEST keyed by (lease_id, bucket_start), so the write is idempotent per bucket: a replayed batch cannot inflate the count. The shipped host-proxy makes a single attempt per batch with no retry, dropping the batch on a non-2xx or transport error (a dropped minute is the accepted lossy-by-design outcome). Auth failures all return one indistinguishable 401 usage-auth-invalid; 400 too-many-reports covers a batch over 1024.

Revoke a lease

POST /v1/validators/{id}/leases/{lease_id}/revoke lets the validator owner terminate one of their leases. The Idempotency-Key header is required (a 26-character ULID), the same mechanics as lease booking.

curl -X POST https://api.swqos.dev/v1/validators/01JXEZV2P9R8S7T6W5X4Y3Z2A1/leases/01JXF2LEASEXAMPLE000000000/revoke \
  -H "Authorization: Bearer $VALIDATOR_API_KEY" \
  -H "Idempotency-Key: 01JXF69ZD0Q1W2E3R4T5Y6A7B8" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Planned maintenance window, hardware swap" }'

The free-text reason lands verbatim on the lease row and in the audit log. The 200 response:

{
  "data": {
    "lease_id": "01JXF2LEASEXAMPLE000000000",
    "status": "revoked",
    "refund_amount_lamports": 1200000000,
    "audit_log_id": "01JXF7AUDITROW000000000000"
  },
  "meta": { "request_id": "01JXF0...", "version": "1.0" }
}

The marketplace stops issuing signing JWTs for the lease immediately, and any already-issued JWT dies at its expiry (10 minutes or less). The prorated refund is booked on the lease row and paid out manually by ops within 24 hours. Errors: 403 forbidden, 404 lease-not-found (no such lease on that validator), 409 lease-already-revoked, 422 lease-not-active.

Reply to a rating

POST /v1/ratings/{rating_id}/reply posts the validator's one public reply to a trader rating. rating_id comes from recent_ratings on the detail endpoint or the dashboard. Body: {"text": "..."}, 1 to 1000 characters. The 200 response echoes {rating_id, reply_text, replied_at}. The reply appears under the rating on the public detail page.

Errors: 400 invalid-reply-text (empty or over 1000 chars), 404 rating-not-found (not a rating on the caller's validator), 409 already-replied (one reply per rating, no edits via the API).

Public stats endpoints

Three anonymous endpoints power the public stats pages. All return the standard envelope.

  • GET /v1/stats/marketplace-health: headline aggregates: active_validators_count, aggregate_uptime_30d_pct, median_latency_p50_ms / median_latency_p95_ms (nullable, fleet medians over 30 days), total_active_leases, total_leases_this_epoch, and total_signs (lifetime TEE signature count).
  • GET /v1/stats/marketplace-history?range=1d|1w|1m|all (default 1m): time-bucketed series with t, cumulative_sol_settled_lamports (running total), daily_sol_revenue_lamports, validators_online, leases_booked, signs_count, and nullable fleet latency p50/p95 per bucket.
  • GET /v1/stats/leader-schedule: cluster overview for the current epoch: epoch, absolute_slot, slot_index, slots_in_epoch, epoch_progress_pct, total_leaders, total_slots, and a ranked leaders list (identity_pubkey, leader_slot_count, share_pct, plus validator_id, display_name, and icon_url when the identity is a registered validator). Returns 502 solana-rpc-unavailable when the RPC is down.

Admin moderation endpoints (suspend, takedown, admin lease revoke, audit log, reputation recompute) are Clerk staff-only and not part of the validator API surface. See Authentication for the RBAC tiers.

Related pages