Skip to main content

Fetch predictions

A client for GET /predictions that fetches cached SNR quantiles (p50, p10, p90, in dB) for a list of link ids. The contract it respects:

  • link_ids is a repeated query parameter, capped at 100 per request (400 too_many_link_ids beyond that), so larger fleets are chunked.
  • An empty items array is not an error: it means no cached predictions exist yet for those links. The client reports it and moves on; retry after the next capture cycle or use live=true for on-demand inference.
  • 429 rate_limited is retried after Retry-After; other 4xx responses stop the client.

Set CONSTELLATION_API_BASE and CONSTELLATION_API_TOKEN first (common setup).

const BASE = process.env.CONSTELLATION_API_BASE ?? 'https://api.constellation.space'
const TOKEN = process.env.CONSTELLATION_API_TOKEN
if (!TOKEN) throw new Error('CONSTELLATION_API_TOKEN is not set')

const LINK_IDS = ['SAT-0012:GS-SEA-01', 'SAT-0013:GS-SVL-02']
const CHUNK = 100

type Prediction = {
link_id: string
horizon_minutes: number
model_version: string
value: { p50: number; p10: number; p90: number }
}
type PredictionSet = { model_family: string; captured_at: string; items: Prediction[] }

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

async function fetchChunk(linkIds: string[], attempt = 0): Promise<PredictionSet> {
const url = new URL(`${BASE}/predictions`)
url.searchParams.set('model_family', 'snr')
url.searchParams.set('horizon_minutes', '15')
for (const id of linkIds) url.searchParams.append('link_ids', id)

const res = await fetch(url, { headers: { 'x-api-key': TOKEN! } })
if (res.status === 429) {
if (attempt >= 5) throw new Error('giving up after 5 rate-limited retries')
await sleep(Number(res.headers.get('retry-after') ?? '5') * 1000)
return fetchChunk(linkIds, attempt + 1)
}
if (!res.ok) throw new Error(`predictions read failed: ${res.status} ${await res.text()}`)
return (await res.json()) as PredictionSet
}

for (let i = 0; i < LINK_IDS.length; i += CHUNK) {
const set = await fetchChunk(LINK_IDS.slice(i, i + CHUNK))
if (set.items.length === 0) {
console.log(`no cached predictions yet for this chunk (set captured_at ${set.captured_at}); ` +
'retry after the next capture or request live=true')
continue
}
for (const p of set.items) {
console.log(`${p.link_id} +${p.horizon_minutes}m ` +
`p50 ${p.value.p50} dB (p10 ${p.value.p10}, p90 ${p.value.p90}) [${p.model_version}]`)
}
}

Expected output:

SAT-0012:GS-SEA-01 +15m p50 12.4 dB (p10 9.1, p90 15.8) [snr-v11]
SAT-0013:GS-SVL-02 +15m p50 8.2 dB (p10 5.6, p90 11.3) [snr-v11]

Treat p10 as the planning floor for link-closure decisions; a wide p10 to p90 spread means the model is uncertain. To get on-demand inference with full provenance instead of the cached set, add live=true to the query and parse the InferencePayload shape (Predictions API reference); the sandbox environment is the natural place to exercise it.