Marketplace downloads, Stripe, Algorand x402, live probes, and ClickHouse query endpoints.
REST API
Authenticated users download marketplace statistics as JSON. Unauthenticated calls get 401. Unpaid catalog downloads get 402 Payment Required with Algorand x402 accepts. Card payments use Stripe.js (STRIPE_PUBLIC_KEY) and a Checkout Session; pass session_id or Stripe-Session to download. Sign in with Clerk, then send the session JWT.
GET /api/data — catalog (aggregated)
Lists statistics other users have offered. Default payload is aggregated (n, min/median/max RTT, quality, age). Raw timestamp, IP, and GPS are present only when the seller checked non-aggregated sale.
GET /api/data
Authorization: Bearer <Clerk session JWT> # optional for catalog; required for ?download=1
200 application/json
{
"object": "catalog",
"listings": [
{
"id": "…",
"kind": "path-10s",
"n": 12,
"rtt_min_ms": 18.2,
"rtt_median_ms": 24.1,
"rtt_max_ms": 41.0,
"quality": "Medium",
"raw": false
}
]
}
Try GET /api/data
GET /api/data?download=1 — download (authenticated)
Returns the listings the caller is allowed to take home. Requires a Clerk session. If the catalog is unpaid, the API answers 402 unless an Algorand payment header or a paid Stripe Checkout session_id is present.
GET /api/data?download=1
Authorization: Bearer <Clerk session JWT>
PAYMENT-SIGNATURE: <x402 payment header, if the quote required payment>
Stripe-Session: cs_… # optional, after Stripe.js Checkout
401 { "error": "sign in required" }
402 { "x402Version": 2, "accepts": [ { "scheme": "exact", "network": "algorand:…", … } ] }
200 { "object": "download", "listings": [ … ], "paid": { "provider": "stripe", "session": "cs_…" } }
Try GET /api/data?download=1
POST /api/data — offer rows for sale
Clerk-authenticated sellers publish the rows they ticked. Set raw: true only after the non-aggregated checkbox (timestamp, IP, GPS + Graphai OÜ liability release).
POST /api/data
Authorization: Bearer <Clerk session JWT>
Content-Type: application/json
{ "raw": false, "listings": [ { "id": "…", "kind": "path-10s", "n": 10, "rtt_median_ms": 22 } ] }
GET|POST /api/x402 — Algorand payment
x402 exact scheme on Algorand TestNet USDC (ASA 10458941). A request without PAYMENT-SIGNATURE returns HTTP 402 and a payment-required header. After the wallet pays, retry with the payment header; the facilitator verifies and settles, then /api/data?download=1 returns the JSON.
POST /api/x402
Content-Type: application/json
{ "action": "quote", "catalog": true }
402
{
"x402Version": 2,
"accepts": [{
"scheme": "exact",
"network": "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
"maxAmountRequired": "120000",
"resource": "/api/data?download=1",
"mimeType": "application/json",
"extra": { "asset": "10458941", "decimals": 6 }
}],
"facilitator": "https://facilitator.goplausible.xyz"
}
POST /api/x402
PAYMENT-SIGNATURE: <base64 payment>
{ "action": "settle", "catalog": true }
200 { "ok": true, "channel": "algorand-x402", "tx": "…", "resource": "/api/data?download=1" }
Try GET /api/x402
GET|POST /api/payouts — Stripe (STRIPE_PUBLIC_KEY)
The browser loads Stripe.js and calls Stripe(STRIPE_PUBLIC_KEY). The publishable key is returned by GET (never the secret). POST creates a Checkout Session with STRIPE_SECRET_KEY; the client then uses redirectToCheckout({ sessionId }) or embedded Checkout with client_secret.
GET /api/payouts
200 {
"object": "stripe",
"publishableKey": "pk_live_…",
"checkoutReady": true,
"js": "https://js.stripe.com/v3/"
}
GET /api/payouts?session_id=cs_…
200 { "publishableKey": "pk_…", "session": { "id": "cs_…", "paid": true, "payment_status": "paid" } }
POST /api/payouts
{ "provider": "stripe", "action": "buy", "ui_mode": "embedded", "row": { "id": "catalog", "listEur": 0.12 } }
200 {
"id": "cs_…",
"client_secret": "cs_…_secret_…",
"publishableKey": "pk_…",
"ui_mode": "embedded"
}
GET /api/data?download=1&session_id=cs_…
Authorization: Bearer <Clerk session JWT>
Stripe-Session: cs_…
Try GET /api/payouts
GET /api/ip — IPv4 / IPv6 of this request
Classifies the address that reached Vercel. fam=ipv4 or fam=ipv6 sets match when the connecting family equals the request.
GET /api/ip
GET /api/ip?fam=ipv6
200 {
"ip": "2001:db8::1",
"family": "ipv6",
"ipv4": null,
"ipv6": "2001:db8::1",
"match": true,
"wanted": "ipv6"
}
Try GET /api/ip
Try ?fam=ipv4
Try ?fam=ipv6
Live probe (no auth)
The current request path, not the marketplace: /?format=json, /api/stats, CSV variants. client.ip_family is ipv4 or ipv6.
ClickHouse analytics (no auth)
Every completed path sample, networkQuality run, HTTP/2, HTTP/3, IPv4, IPv6, and video test is stored in ClickHouse table graphai.check_wireguard_analytics (EU, europe-west2). Map and table sites share the same runs via graphai.wg_shared (POST/GET /api/share), so wireguard-map and wireguard-data load the checker dataset without relying on postMessage. Named SQL queries are exposed as HTTP GET endpoints.
List queries
Download Postman collection
Download OpenAPI
Postman: Import → File → pick the Postman collection JSON. Insomnia / Bruno / Hoppscotch: import the OpenAPI file. Same files at /clickhouse/postman.json and /clickhouse/openapi.json.
POST /api/share
Content-Type: application/json
credentials: include
{ "dataset": { "dwells": [ … ] }, "kinds": ["all","geo","plain"], "clerkUserId": "user_…" }
200 { "ok": true, "source": "clickhouse", "kinds": ["all","geo","plain"] }
GET /api/share?kind=geo
credentials: include
200 { "ok": true, "dataset": { "dwells": [ … ] }, "source": "clickhouse" }
POST /api/clickhouse
Content-Type: application/json
{ "kind": "path-10s", "at": "…", "snapshot": { "client": {…}, "wireguard": {…}, "browser": {…} } }
200 { "ok": true, "inserted": 1, "table": "graphai.check_wireguard_analytics" }
GET /api/queries
200 {
"object": "query-catalog",
"downloads": {
"postman": "https://check-wireguard.graphai.eu/api/queries?download=postman",
"openapi": "https://check-wireguard.graphai.eu/api/queries?download=openapi"
},
"queries": [
{ "name": "by_country", "href": "/api/queries?name=by_country" }
]
}
GET /api/queries?name=by_country&days=30
GET /api/queries/by_isp?days=7
GET /api/queries?name=scans_recent&limit=50
GET /api/queries?name=wireguard_share
GET /api/queries?name=rtt_percentiles
GET /api/queries?name=geo_points
GET /api/queries?name=hourly_volume
GET /api/queries?name=by_protocol
GET /api/queries?name=by_kind
GET /api/queries?name=quality_mix
GET /api/queries?name=dns_filter
GET /api/queries?name=logs_recent
GET /api/queries?name=logs_by_app
GET /api/queries?name=logs_errors
POST /api/logs
Content-Type: application/json
{ "app": "jobs.graphai.eu", "logs": [{ "level": "error", "message": "…", "href": "https://jobs.graphai.eu/" }] }
Try by_country
Try wireguard_share
Try scans_recent