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
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /v1/validators | public | browse with filters, cursor-paginated |
GET | /v1/validators/{id} | public | detail + availability + public history |
GET | /v1/validators/{id}/leader-schedule | public | leader slots, current epoch |
GET | /v1/validators/stake-info | public | pre-registration stake check |
POST | /v1/validators | signed challenge | register, raw response |
PUT | /v1/validators/payout-wallet | signed challenge | raw response |
POST | /v1/validators/onboarding-sessions | bearer | web-funnel token mint |
GET | /v1/validators/onboarding-sessions/{token} | bearer | poll funnel status |
GET | /v1/validators/me | bearer | owned validators |
GET | /v1/validators/{id}/dashboard | bearer (owner) | revenue, leases, security panel |
GET | /v1/validators/{id}/history | bearer (owner) | series including revenue |
PATCH | /v1/validators/{id} | bearer (owner) | price, bio, endpoint, shared config |
POST | /v1/leases/{id}/usage | bearer (lease JWT) | host-proxy sign-count ingest (always-on telemetry) |
POST | /v1/validators/{id}/leases/{lease_id}/revoke | bearer (owner) + Idempotency-Key | revoke + prorated refund |
POST | /v1/ratings/{rating_id}/reply | bearer (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 param | Type | Default | Description |
|---|---|---|---|
region | string | none | Exact AWS region match, e.g. us-east-1 |
price_min_lamports | u64 | none | Minimum exclusive price per epoch |
price_max_lamports | u64 | none | Maximum exclusive price per epoch |
available_now | boolean | false | Only validators free for the next 14-epoch window |
cursor | string | none | Opaque cursor from a previous page |
limit | integer | 20 | Page 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:
| Field | Type | Description |
|---|---|---|
website_url | string or null | Validator-supplied website link |
stake_percentile | number | Percent rank of stake across live validators, 0.0 to 1.0 |
slots_per_epoch | integer | Solana constant, 432000 |
leader_time_seconds_per_epoch | number | Expected leader time per epoch from the stake fraction |
latency_p50_ms / latency_p95_ms | number or null | 30-day sign-probe latency, null before first probe |
recent_ratings | array | Top 5 newest ratings: {rating_id, stars, comment, rated_at, validator_reply_text, validator_replied_at} |
current_epoch | integer | Current Solana epoch from the API clock |
next_available_epoch | integer | See below |
epoch_availability | array | Sparse occupancy, see below |
history | object | Public 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 }
]
| Field | Meaning |
|---|---|
epoch | Absolute Solana epoch number |
exclusive_booked | true when an exclusive lease covers this epoch (blocks both modes) |
shared_taken | Count 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
}'
| Field | Required | Description |
|---|---|---|
pubkey | yes | Validator Ed25519 identity pubkey, base58 (32 bytes) |
payout_wallet | yes | Base58 wallet the 90% payment leg credits, signed into the challenge |
endpoint_url | yes | gRPC endpoint URL of the TEE host-proxy |
price_sol | yes | Exclusive asking price per epoch in SOL, converted to lamports server-side |
bio | yes | Public listing bio |
nonce_hex | yes | 32 hex chars (16 bytes), anti-replay |
timestamp | yes | Unix seconds at signing time |
signature_hex | yes | 128 hex chars, Ed25519 signature over the challenge |
onboard_token | no | One-time token from the web funnel, attributes the listing to the operator's account |
region | no | AWS region of the TEE, defaults to us-east-1 |
tier | no | economy, standard (default), or performance |
shared_price_sol | no | Shared price per epoch in SOL, enables shared together with the seat cap |
max_concurrent_shared_leases | no | Shared 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:
| Status | Slug | When |
|---|---|---|
| 400 | register-timestamp-skew | timestamp outside the 5-minute window |
| 400 | register-invalid-field | Malformed pubkey, wallet, nonce, signature, or a negative/non-finite price |
| 401 | register-signature-invalid | Ed25519 verification failed |
| 409 | register-nonce-replay | Nonce already used |
| 409 | validator-already-registered | Pubkey already has a listing |
| 422 | register-price-required | price_sol is 0 (a live listing needs a price) |
| 422 | register-invalid-tier | Tier not in the enum |
| 422 | register-invalid-region | Unknown region |
| 422 | not-a-solana-validator | Pubkey not in getVoteAccounts() |
| 422 | invalid-onboarding-token | Funnel token unknown, expired, or used |
| 422 | register-shared-config-incomplete | Only one of the shared pair provided |
| 422 | register-shared-price-invalid | Shared price not positive |
| 422 | register-shared-seats-invalid | Seat cap below 1 |
| 503 | solana-rpc-unavailable | Stake 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:
| Field | Description |
|---|---|
revenue_lamports_30d | Gross lease totals activated in the trailing 30 days |
net_revenue_lamports_30d | Sum of confirmed validator_share legs over 30 days, the net the payout wallet actually received |
fee_bps | Current marketplace fee in basis points (1000 = 10%) |
payout_wallet_address | Wallet the 90% leg credits today |
active_leases | Active leases: {lease_id, trader_pubkey, epoch_start, epoch_end, status, total_amount_lamports, signs_count} |
signs_count_30d | Real TEE signature volume served over 30 days |
tee_health | healthy when the listing is live, otherwise down |
current_epoch | Current Solana epoch |
recent_payments | Confirmed settlements: gross amount, both split legs, confirmed_at, and the on-chain tx signature |
security | Security-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 }'
| Field | Constraints | Semantics |
|---|---|---|
price_sol | > 0 | New exclusive price. Takes effect at the next epoch boundary, booked leases keep their frozen price |
bio | string | Replaces the public bio |
endpoint_url | string | New TEE host-proxy URL |
shared_enabled | boolean | Turns the shared offering on or off |
shared_price_sol | > 0 | Shared price per epoch, independent of the exclusive price |
max_concurrent_shared_leases | >= 1 | Shared seat cap per epoch |
Shared-offering semantics:
- Setting
shared_enabled: truerequires a shared price and a seat cap, either in the same patch or already stored on the listing. Otherwise the call fails with422 shared-config-incomplete. - Setting
shared_enabled: falsestops 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_leasesnever 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 getsmax(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
}
]
}
| Field | Constraints |
|---|---|
reports | 1024 max per batch. An empty batch returns 200 with accepted: 0 |
bucket_start | RFC 3339, aligned to floor(now, 60s) by the reporter |
signs_delta | Successful 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, andtotal_signs(lifetime TEE signature count).GET /v1/stats/marketplace-history?range=1d|1w|1m|all(default1m): time-bucketed series witht,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 rankedleaderslist (identity_pubkey,leader_slot_count,share_pct, plusvalidator_id,display_name, andicon_urlwhen the identity is a registered validator). Returns502 solana-rpc-unavailablewhen 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
The full register flow: stake check, web funnel, validator-cli, and going live.
How exclusive and shared pricing, the 10% fee, and the payout wallet work.
The trader side: booking, Solana Pay payment, credentials, and signing tokens.
RFC 7807 problem documents and the full slug catalogue.