← Meteor Tally
API
Tokens

Drive Meteor Tally from your own code

Both metered steps - the watch plan and the observing report - are available over HTTP. A natural use is an astronomy club's page or a stargazing newsletter: your code builds the night (or a member's tally), sends it, and shows the answer.

The split first: the page does the arithmetic; the model writes the judgement. Sun and Moon positions, the radiant's altitude, the limiting-magnitude and rate estimates, the best window, the facing, the ZHR and its exact Poisson interval are all computed by meteorkit.js, which runs in node as well as in the browser. The model reads those numbers; it never redoes them.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"meteor-tally"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both tasks are metered and need a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task, which routes the one system prompt to one of two contracts:

taskWhat it returnsVerdicts
planWatch plan, before the night: a summary; a window (start, end, why) inside the dark hours; a face (compass, altitude_deg, why); an expect range (low, high, note); 4–8 schedule steps {time, do}; kit; eyes-and-comfort tips; risks; 0–2 backup nights {date, why} inside the activity window.go, short-window, skip-tonight
reportObserving report, after the count: a summary; zhr (value, low, high quoted from the input totals, and a plain read); one periods entry per input period {id, read, flag} with flag ok, caution or drop; versus the expected activity; caveats; next-time improvements; a share text.solid, usable, too-thin

The declared input schema carries scalars only, so object- and array-valued fields travel JSON-encoded as strings (MeteorKit.toWire does this). task, night_date, shower and site are required.

Input fields

FieldTypeNotes
taskstringplan or report.
night_datestringLocal date of the evening the watch starts; times before 12:00 are the next morning.
showerstring (JSON){code, name, parent, peak, days_from_peak, active, active_window, zhr_peak_est, zhr_tonight_est, speed_kms, population_index, radiant_ra_deg, radiant_dec_deg} - approximate published values.
sitestring (JSON){name, lat, lon, utc_offset_h, sky, sky_lm_moonless}.
notesstringThe observer's own words; over 1,400 characters keeps the first 1,000 and last 400 around a marker.
hours_availablenumberplan only: 1–12.
factsstring (JSON)plan only: {sunset, sunrise, dark_from, dark_to, astro_dark, moon:{phase, illum_pct, events[]}, hours:[{hour, dark, radiant_alt, moon_alt, lm_est, rate_est}], best_window, look, expected_night_est, checks[]}.
periodsstring (JSON)report only: [{id, start, end, break_min, teff_h, shower_n, sporadic_n, lm, cloud_pct, radiant_alt, zhr, zhr_ci95, flags[]}]; zhr is null when the method does not apply.
totalsstring (JSON)report only: {shower_n, sporadic_n, teff_h, zhr, zhr_ci95, n_used, expected_zhr_tonight_est}.
brightnessstring (JSON)report only: {bright, medium, faint} counts.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with MeteorKit.parseReply, then normalize, then check it with reconcile(reply, {analysis, tally}): it flags a window that starts before dark or runs into dawn, a window outside the best hours, an expected count far above the page's estimate, a facing that is not a compass point or differs from the page's pick, backup nights outside the activity window, a "go" on a night with a hard problem, a report ZHR outside the page's 95% interval, unknown or unread periods, and "solid" on fewer than 20 meteors.

Worked examples

Replies produced against the app's prompt (long lists shortened here; the full replies ship in example-result.js). The page replays each of them free from its example buttons. Inputs are shown with the hour table trimmed.

task: plan - Orionid peak from a rural valley, Colorado (go)

Input:

{
 "task": "plan",
 "night_date": "2026-10-21",
 "shower": "{\"code\":\"ORI\",\"name\":\"Orionids\",\"parent\":\"comet 1P/Halley\",\"peak\":\"2026-10-21\",\"days_from_peak\":0,\"active\":true,\"active_window\":\"2026-10-02 to 2026-11-07\",\"zhr_peak_est\":20,\"zhr_tonight_est\":20,\"speed_kms\":66,\"population_index\":2.5,\"radiant_ra_deg\":95,\"radiant_dec_deg\":16}",
 "site": "{\"name\":\"Wet Mountain Valley, Colorado\",\"lat\":38.1,\"lon\":-105.5,\"utc_offset_h\":-6,\"sky\":\"Rural\",\"sky_lm_moonless\":6}",
 "notes": "Driving up from Denver with a friend. We can be out from about 2 am until dawn. Forecast is clear and near freezing.",
 "hours_available": 4,
 "facts": "{\"sunset\":\"18:15\",\"sunrise\":\"07:17\",\"dark_from\":\"19:13\",\"dark_to\":\"06:20\",\"astro_dark\":true,\"moon\":{\"phase\":\"waxing gibbous\",\"illum_pct\":83,\"events\":[\"sets 03:23\"]},\"hours\":[{\"hour\":\"19:00\",\"dark\":true,\"radiant_alt\":-28,\"moon_alt\":35,\"lm_est\":4.3,\"rate_est\":0},{\"hour\":\"20:00\",\"dark\":true,\"radiant_alt\":-20,\"moon_alt\":41,\"lm_est\":4.7,\"rate_est\":0},{\"hour\":\"21:00\",\"dark\":true,\"radiant_alt\":-10,\"moon_alt\":43,\"lm_est\":4.7,\"rate_est\":0},\"...\"],\"best_window\":{\"start\":\"03:00\",\"end\":\"06:00\",\"peak_hour\":\"05:00\",\"peak_rate_est\":11.7,\"expected_est\":34},\"look\":{\"compass\":\"WNW\",\"altitude_deg\":55,\"from_radiant_deg\":45,\"from_moon_deg\":null},\"expected_night_est\":44,\"checks\":[{\"level\":\"info\",\"code\":\"peak-night\",\"text\":\"Peak night of the Orionids (ZHR about 20, an estimate).\"},{\"level\":\"info\",\"code\":\"expect\",\"text\":\"Best window 03:00-06:00: about 34 shower meteors for one observer (an estimate), peaking near 11.7/hr around 05:00.\"},\"...\"]}"
}

Reply:

{
 "task": "plan",
 "title": "Orionids peak night - go for it after 2 AM",
 "verdict": "go",
 "summary": "Peak night for the Orionids, and from Wet Mountain Valley's dark rural sky this is a strong watch. The radiant climbs through Orion from 2 AM onward and the bright gibbous moon sets by 3:23 AM, so the hourly rate keeps rising right into dawn. Near-freezing temps mean this is as much about staying warm as staying awake.",
 "window": {
  "start": "02:00",
  "end": "06:00",
  "why": "The best 4-hour stretch inside your available time: radiant altitude climbs from 47 to 68 degrees and the bright moon has set by 03:23, so the rate keeps rising through dawn."
 },
 "face": {
  "compass": "WNW",
  "altitude_deg": 55,
  "why": "The page's look direction keeps the radiant about 45 degrees off to your side so streaks and trains show up well across your field of view."
 },
 "expect": {
  "low": 32,
  "high": 42,
  "note": "Summing the page's hourly rate estimates for 02:00-06:00 gives about 39 shower meteors for one observer; a few sporadics (meteors not from this shower) could add a little more. This is a rough estimate, not a promise."
 },
 "schedule": [
  {
   "time": "01:15",
   "do": "Leave with time to spare on the rural roads in the dark."
  },
  {
   "time": "01:45",
   "do": "Arrive, set up facing WNW, and start a 20-minute dark adaptation - phones away."
  },
  "..."
 ],
 "kit": [
  "insulated layers and a hat - it's near freezing",
  "hot drink in a thermos",
  "..."
 ],
 "eyes": [
  "give yourself a full 20 minutes of darkness before counting - no phone screens",
  "use only red light for notes or maps",
  "..."
 ],
 "risks": [
  "near-freezing temperatures - watch for numb hands and feet",
  "dark rural roads driving in - go slowly and watch for wildlife",
  "fatigue on the drive back to Denver after being up all night"
 ],
 "backup": [
  {
   "date": "2026-10-22",
   "why": "Still essentially peak-level activity the next night, in case clouds move in; the moon will be slightly fuller, which isn't factored in here."
  }
 ]
}

task: report - Orionid peak from a rural valley, Colorado (solid)

Input:

{
 "task": "report",
 "night_date": "2026-10-21",
 "shower": "{\"code\":\"ORI\",\"name\":\"Orionids\",\"parent\":\"comet 1P/Halley\",\"peak\":\"2026-10-21\",\"days_from_peak\":0,\"active\":true,\"active_window\":\"2026-10-02 to 2026-11-07\",\"zhr_peak_est\":20,\"zhr_tonight_est\":20,\"speed_kms\":66,\"population_index\":2.5,\"radiant_ra_deg\":95,\"radiant_dec_deg\":16}",
 "site": "{\"name\":\"Wet Mountain Valley, Colorado\",\"lat\":38.1,\"lon\":-105.5,\"utc_offset_h\":-6,\"sky\":\"Rural\",\"sky_lm_moonless\":6}",
 "notes": "Driving up from Denver with a friend. We can be out from about 2 am until dawn. Forecast is clear and near freezing.",
 "periods": "[{\"id\":\"P1\",\"start\":\"03:00\",\"end\":\"04:00\",\"break_min\":0,\"teff_h\":1,\"shower_n\":10,\"sporadic_n\":4,\"lm\":6,\"cloud_pct\":0,\"radiant_alt\":57,\"zhr\":18.8,\"zhr_ci95\":[9,34.5],\"flags\":[]},{\"id\":\"P2\",\"start\":\"04:00\",\"end\":\"05:00\",\"break_min\":5,\"teff_h\":0.92,\"shower_n\":13,\"sporadic_n\":5,\"lm\":6.1,\"cloud_pct\":0,\"radiant_alt\":65,\"zhr\":22.5,\"zhr_ci95\":[12,38.5],\"flags\":[]},{\"id\":\"P3\",\"start\":\"05:00\",\"end\":\"05:50\",\"break_min\":0,\"teff_h\":0.83,\"shower_n\":9,\"sporadic_n\":2,\"lm\":5.9,\"cloud_pct\":10,\"radiant_alt\":68,\"zhr\":22.5,\"zhr_ci95\":[10.3,42.6],\"flags\":[]}]",
 "totals": "{\"shower_n\":32,\"sporadic_n\":11,\"teff_h\":2.75,\"zhr\":21.2,\"zhr_ci95\":[14.5,29.9],\"n_used\":32,\"expected_zhr_tonight_est\":20}",
 "brightness": "{\"bright\":6,\"medium\":14,\"faint\":12}"
}

Reply:

{
 "task": "report",
 "title": "A strong Orionids night, well earned",
 "verdict": "solid",
 "summary": "You hit peak night under genuinely dark, rural skies - limiting magnitudes around 6 in all three periods - and stayed out through the best pre-dawn hours as the radiant climbed from 57 to 68 degrees. With 32 shower meteors over 2 hours 45 minutes of effective time, the overall ZHR comes out to about 21, a solid result that lines up well with the roughly 20 expected for the Orionids tonight.",
 "zhr": {
  "value": 21.2,
  "low": 14.5,
  "high": 29.9,
  "read": "The overall ZHR is 21.2, with a 95% range of 14.5 to 29.9. That spread comes from Poisson counting statistics: with 32 meteors behind it, the relative uncertainty is roughly 1 over the square root of 32, about 18 percent, so the interval is already fairly narrow and would tighten further with more meteors."
 },
 "periods": [
  {
   "id": "P1",
   "read": "10 shower meteors in a full 60 minutes with the radiant already 57 degrees up and a clean, cloud-free sky - a solid hour on its own, ZHR 18.8 (9 to 34.5).",
   "flag": "ok"
  },
  {
   "id": "P2",
   "read": "The best hour of the night: 13 shower meteors in 55 effective minutes as the radiant climbed to 65 degrees, pushing the hourly ZHR to about 22.5.",
   "flag": "ok"
  },
  {
   "id": "P3",
   "read": "Light cloud crept in (10 percent) in the final, shorter stretch, but it still turned up 9 shower meteors in 50 minutes before dawn, holding the ZHR near 22.5.",
   "flag": "ok"
  }
 ],
 "versus": "The night's ZHR of 21.2 sits right at the roughly 20 the Orionids were expected to produce at peak, and 20 falls comfortably inside the measured interval - a clean consistent-with-forecast result, no explaining away needed.",
 "caveats": [
  "Individual hours only had 9 to 13 shower meteors each, so the hour-by-hour swing (18.8 to 22.5) is mostly counting noise, not a real change in activity.",
  "A 5-minute break before P2 and 10 percent cloud in P3 trimmed a little effective time from those hours; that is already reflected in their teff_h.",
  "..."
 ],
 "next_time": [
  "You were on site by about 2:00 but counting did not start until 3:00 - starting the formal tally as soon as you are dark-adapted would add close to another hour of data.",
  "Keep breaks between periods as short as you can; even 5 minutes trims effective time and widens the interval a touch.",
  "..."
 ],
 "share": "Orionids watch from Wet Mountain Valley, Colorado on the night of Oct 21, 2026 - a rural, dark site. Over 2 hours 45 minutes effective time (3:00 to 5:50 am) we tallied 32 shower meteors plus 11 sporadics, for a ZHR of 21.2 (95% range 14.5-29.9) - right on the expected peak rate of about 20."
}

task: plan - Perseid peak from a suburban yard, two hours max (short-window)

Input:

{
 "task": "plan",
 "night_date": "2026-08-12",
 "shower": "{\"code\":\"PER\",\"name\":\"Perseids\",\"parent\":\"comet 109P/Swift-Tuttle\",\"peak\":\"2026-08-12\",\"days_from_peak\":0,\"active\":true,\"active_window\":\"2026-07-17 to 2026-08-24\",\"zhr_peak_est\":100,\"zhr_tonight_est\":100,\"speed_kms\":59,\"population_index\":2.2,\"radiant_ra_deg\":48,\"radiant_dec_deg\":58}",
 "site": "{\"name\":\"Back yard north of New York\",\"lat\":40.95,\"lon\":-73.8,\"utc_offset_h\":-4,\"sky\":\"Suburb or town edge\",\"sky_lm_moonless\":5}",
 "notes": "Back yard, the neighbours leave porch lights on. Work the next day, so two hours at most, 10 pm to midnight. Kids aged 9 and 12 want to see at least one.",
 "hours_available": 2,
 "facts": "{\"sunset\":\"19:57\",\"sunrise\":\"06:03\",\"dark_from\":\"21:03\",\"dark_to\":\"04:57\",\"astro_dark\":true,\"moon\":{\"phase\":\"new\",\"illum_pct\":0,\"events\":[\"sets 20:01\"]},\"hours\":[{\"hour\":\"20:00\",\"dark\":false,\"radiant_alt\":12,\"moon_alt\":-5,\"lm_est\":2.5,\"rate_est\":0},{\"hour\":\"21:00\",\"dark\":true,\"radiant_alt\":15,\"moon_alt\":-15,\"lm_est\":4.7,\"rate_est\":6.3},{\"hour\":\"22:00\",\"dark\":true,\"radiant_alt\":20,\"moon_alt\":-24,\"lm_est\":5,\"rate_est\":10.4},\"...\"],\"best_window\":{\"start\":\"00:00\",\"end\":\"04:57\",\"peak_hour\":\"03:00\",\"peak_rate_est\":25.3,\"expected_est\":106},\"look\":{\"compass\":\"NW\",\"altitude_deg\":55,\"from_radiant_deg\":47,\"from_moon_deg\":null},\"expected_night_est\":136,\"checks\":[{\"level\":\"info\",\"code\":\"peak-night\",\"text\":\"Peak night of the Perseids (ZHR about 100, an estimate).\"},{\"level\":\"info\",\"code\":\"expect\",\"text\":\"Best window 00:00-04:57: about 106 shower meteors for one observer (an estimate), peaking near 25.3/hr around 03:00.\"}]}"
}

Reply:

{
 "task": "plan",
 "title": "A solid hour or two before the Perseids really get going",
 "verdict": "short-window",
 "summary": "Peak night for the Perseids, but your 10 PM to midnight window catches the shower before it really ramps up - the radiant is only 20 to 26 degrees high then, well short of its best around 3 AM. Even so, with a new moon and clear dark skies from 9 PM, you should still see a steady trickle, more than enough for the kids to catch several.",
 "window": {
  "start": "22:00",
  "end": "00:00",
  "why": "This matches the two hours you have before bed and work tomorrow; the rate is already climbing nicely by 10 PM even though the real peak comes hours after you'll be inside."
 },
 "face": {
  "compass": "NW",
  "altitude_deg": 55,
  "why": "This is the page's recommended direction for tonight's radiant geometry; if a porch light sits that way, just turn your chairs slightly and shield your eyes with a cap brim."
 },
 "expect": {
  "low": 18,
  "high": 28,
  "note": "The page's hourly rates for 22:00-00:00 add up to about 24 shower meteors for one observer; a few sporadics could nudge that higher. It's an estimate, but the kids should see at least one within the first 15-20 minutes on a night like this."
 },
 "schedule": [
  {
   "time": "21:40",
   "do": "Head outside, get comfortable, and start a 20-minute no-screens dark adaptation."
  },
  {
   "time": "22:00",
   "do": "Start watching, facing NW, reclined so nobody has to crane their neck."
  },
  "..."
 ],
 "kit": [
  "reclining lawn chairs or blankets on the grass",
  "warm layers for the overnight temperature drop",
  "..."
 ],
 "eyes": [
  "20 minutes with no screens before you start counting",
  "turn away from the porch-light glare if it bothers anyone",
  "..."
 ],
 "risks": [
  "watch for trip hazards like yard furniture or hoses in the dark",
  "keep an eye on the clock - it's a school and work night for everyone"
 ],
 "backup": [
  {
   "date": "2026-08-13",
   "why": "Activity is still near its peak the following night and the moon stays out of the way; not computed here, but worth a repeat if clouds interfere tonight."
  }
 ]
}

task: report - Perseid peak from a suburban yard, two hours max (usable)

Input:

{
 "task": "report",
 "night_date": "2026-08-12",
 "shower": "{\"code\":\"PER\",\"name\":\"Perseids\",\"parent\":\"comet 109P/Swift-Tuttle\",\"peak\":\"2026-08-12\",\"days_from_peak\":0,\"active\":true,\"active_window\":\"2026-07-17 to 2026-08-24\",\"zhr_peak_est\":100,\"zhr_tonight_est\":100,\"speed_kms\":59,\"population_index\":2.2,\"radiant_ra_deg\":48,\"radiant_dec_deg\":58}",
 "site": "{\"name\":\"Back yard north of New York\",\"lat\":40.95,\"lon\":-73.8,\"utc_offset_h\":-4,\"sky\":\"Suburb or town edge\",\"sky_lm_moonless\":5}",
 "notes": "Back yard, the neighbours leave porch lights on. Work the next day, so two hours at most, 10 pm to midnight. Kids aged 9 and 12 want to see at least one.",
 "periods": "[{\"id\":\"P1\",\"start\":\"22:00\",\"end\":\"23:00\",\"break_min\":0,\"teff_h\":1,\"shower_n\":6,\"sporadic_n\":3,\"lm\":5,\"cloud_pct\":20,\"radiant_alt\":20,\"zhr\":72.1,\"zhr_ci95\":[26.5,157],\"flags\":[\"radiant low (under 20\\u00b0)\"]},{\"id\":\"P2\",\"start\":\"23:00\",\"end\":\"00:00\",\"break_min\":10,\"teff_h\":0.83,\"shower_n\":9,\"sporadic_n\":2,\"lm\":5.1,\"cloud_pct\":30,\"radiant_alt\":26,\"zhr\":107.2,\"zhr_ci95\":[49,203.5],\"flags\":[\"over 20% cloud\"]}]",
 "totals": "{\"shower_n\":15,\"sporadic_n\":5,\"teff_h\":1.83,\"zhr\":89.7,\"zhr_ci95\":[50.2,148],\"n_used\":15,\"expected_zhr_tonight_est\":100}",
 "brightness": "{\"bright\":4,\"medium\":7,\"faint\":4}"
}

Reply:

{
 "task": "report",
 "title": "A real Perseid count, with honest caveats",
 "verdict": "usable",
 "summary": "Fifteen shower meteors in under two hours from a porch-lit suburban yard, with the radiant only 20 to 26 degrees up, still produces a usable ZHR of about 90 - a real result, but built on a modest count with the sky and radiant working against you. It lines up with the roughly 100 expected on peak night, just with a wide margin either way.",
 "zhr": {
  "value": 89.7,
  "low": 50.2,
  "high": 148,
  "read": "The overall ZHR is 89.7, with a 95% range of 50.2 to 148 - a wide band because it rests on just 15 shower meteors. Poisson counting means the relative uncertainty scales with 1 over the square root of 15, around 26 percent, and that spread is exactly what shows up between 50 and 148."
 },
 "periods": [
  {
   "id": "P1",
   "read": "6 shower meteors with the radiant only 20 degrees up, low enough that the page flagged it - a low radiant inflates both the correction and its uncertainty.",
   "flag": "caution"
  },
  {
   "id": "P2",
   "read": "9 shower meteors as the radiant rose a little to 26 degrees, but cloud reached 30 percent, trimming effective time and adding its own noise.",
   "flag": "caution"
  }
 ],
 "versus": "At 89.7 the result sits a bit under the roughly 100 expected for peak night, but 100 still falls inside the measured interval, so this reads as consistent with expectations once you allow for the small sample, the low radiant, and the cloud.",
 "caveats": [
  "Only 15 shower meteors total across two short hours - the swing from 72 to 107 hour to hour is mostly noise, not a real change in the shower.",
  "The radiant was low the whole session (20 to 26 degrees), which the page's flag and the correction factor both reflect; low-radiant ZHRs run noisier than high-radiant ones.",
  "..."
 ],
 "next_time": [
  "A later or longer window on another peak night would catch the radiant higher up, with a less extreme correction.",
  "A site even a little darker than the porch-lit yard would raise the limiting magnitude and pick up fainter meteors.",
  "..."
 ],
 "share": "Perseids watch from a suburban yard north of New York City on the night of Aug 12, 2026 - porch lights and patchy cloud. Over 1 hour 50 minutes effective time (10 pm to midnight) we tallied 15 shower meteors plus 5 sporadics, for a ZHR of about 90 (95% range 50-148), broadly in line with the roughly 100 expected at peak."
}

task: plan - Leonids from a London rooftop (skip-tonight)

Input:

{
 "task": "plan",
 "night_date": "2026-11-17",
 "shower": "{\"code\":\"LEO\",\"name\":\"Leonids\",\"parent\":\"comet 55P/Tempel-Tuttle\",\"peak\":\"2026-11-17\",\"days_from_peak\":0,\"active\":true,\"active_window\":\"2026-11-06 to 2026-11-30\",\"zhr_peak_est\":15,\"zhr_tonight_est\":15,\"speed_kms\":71,\"population_index\":2.5,\"radiant_ra_deg\":152,\"radiant_dec_deg\":22}",
 "site": "{\"name\":\"Flat roof in east London\",\"lat\":51.54,\"lon\":-0.06,\"utc_offset_h\":0,\"sky\":\"City centre\",\"sky_lm_moonless\":4}",
 "notes": "Flat roof, can go up after midnight for a couple of hours. Light pollution is bad - I can see maybe 15 stars.",
 "hours_available": 2,
 "facts": "{\"sunset\":\"16:08\",\"sunrise\":\"07:23\",\"dark_from\":\"17:26\",\"dark_to\":\"06:04\",\"astro_dark\":true,\"moon\":{\"phase\":\"first quarter\",\"illum_pct\":55,\"events\":[\"sets 23:25\"]},\"hours\":[{\"hour\":\"17:00\",\"dark\":true,\"radiant_alt\":-16,\"moon_alt\":25,\"lm_est\":2.5,\"rate_est\":0},{\"hour\":\"18:00\",\"dark\":true,\"radiant_alt\":-16,\"moon_alt\":26,\"lm_est\":3.4,\"rate_est\":0},{\"hour\":\"19:00\",\"dark\":true,\"radiant_alt\":-15,\"moon_alt\":24,\"lm_est\":3.5,\"rate_est\":0},\"...\"],\"best_window\":{\"start\":\"02:00\",\"end\":\"06:00\",\"peak_hour\":\"04:00\",\"peak_rate_est\":1.2,\"expected_est\":4},\"look\":{\"compass\":\"NE\",\"altitude_deg\":55,\"from_radiant_deg\":48,\"from_moon_deg\":null},\"expected_night_est\":6,\"checks\":[{\"level\":\"info\",\"code\":\"peak-night\",\"text\":\"Peak night of the Leonids (ZHR about 15, an estimate).\"},{\"level\":\"info\",\"code\":\"expect\",\"text\":\"Best window 02:00-06:00: about 4 shower meteors for one observer (an estimate), peaking near 1.2/hr around 04:00.\"},\"...\"]}"
}

Reply:

{
 "task": "plan",
 "title": "Leonids: a city roof won't show much tonight",
 "verdict": "skip-tonight",
 "summary": "The Leonids peak tonight, but from a light-polluted central London roof where you can only make out about 15 stars, the page's own numbers put the expected count at roughly 2 meteors an hour even in the best slot - well under what makes a watch worthwhile. A first-quarter moon also washes out the sky until it sets at 23:25. This one isn't worth losing sleep over from this site.",
 "window": {
  "start": "04:00",
  "end": "06:00",
  "why": "If you go up anyway, this is the best 2-hour slot the page's numbers support - radiant highest, moon long since down - though rates still barely top 1 per hour."
 },
 "face": {
  "compass": "NE",
  "altitude_deg": 55,
  "why": "This is the page's recommended direction toward the radiant for whatever faint streaks do appear."
 },
 "expect": {
  "low": 1,
  "high": 3,
  "note": "Summing the page's hourly rates for 04:00-06:00 gives roughly 2 shower meteors for one observer - city glow and moonlight both cut hard into what this modest shower (ZHR about 15) offers. This is a rough estimate and you could easily see none."
 },
 "schedule": [
  {
   "time": "00:00",
   "do": "Consider skipping tonight's watch altogether and picking a darker night or site instead (see backup below)."
  },
  {
   "time": "03:40",
   "do": "If you still want to try, head up and give your eyes 15-20 minutes to adjust, though city glow limits how dark-adapted you can get."
  },
  "..."
 ],
 "kit": [
  "warm coat, hat and gloves - a November roof is cold",
  "something to sit or lean on",
  "..."
 ],
 "eyes": [
  "15-20 minutes to adapt, though a city sky limits how much it helps",
  "avoid phone and street-light glare while watching",
  "..."
 ],
 "risks": [
  "stay well clear of the roof edge, especially in the dark and cold",
  "watch for frost making the roof surface slippery"
 ],
 "backup": [
  {
   "date": "2026-11-20",
   "why": "Still inside the Leonids' active window with a less intrusive moon; the page's own note suggests a short trip to darker sky would help far more than waiting for a different night."
  }
 ]
}

task: report - Leonids from a London rooftop (too-thin)

Input:

{
 "task": "report",
 "night_date": "2026-11-17",
 "shower": "{\"code\":\"LEO\",\"name\":\"Leonids\",\"parent\":\"comet 55P/Tempel-Tuttle\",\"peak\":\"2026-11-17\",\"days_from_peak\":0,\"active\":true,\"active_window\":\"2026-11-06 to 2026-11-30\",\"zhr_peak_est\":15,\"zhr_tonight_est\":15,\"speed_kms\":71,\"population_index\":2.5,\"radiant_ra_deg\":152,\"radiant_dec_deg\":22}",
 "site": "{\"name\":\"Flat roof in east London\",\"lat\":51.54,\"lon\":-0.06,\"utc_offset_h\":0,\"sky\":\"City centre\",\"sky_lm_moonless\":4}",
 "notes": "Flat roof, can go up after midnight for a couple of hours. Light pollution is bad - I can see maybe 15 stars.",
 "periods": "[{\"id\":\"P1\",\"start\":\"01:00\",\"end\":\"02:00\",\"break_min\":0,\"teff_h\":1,\"shower_n\":2,\"sporadic_n\":1,\"lm\":4,\"cloud_pct\":30,\"radiant_alt\":28,\"zhr\":60.4,\"zhr_ci95\":[7.3,218.3],\"flags\":[\"bright sky (limiting magnitude under 5)\",\"over 20% cloud\"]},{\"id\":\"P2\",\"start\":\"02:00\",\"end\":\"02:40\",\"break_min\":0,\"teff_h\":0.67,\"shower_n\":1,\"sporadic_n\":1,\"lm\":4.1,\"cloud_pct\":40,\"radiant_alt\":36,\"zhr\":38.7,\"zhr_ci95\":[1,215.6],\"flags\":[\"bright sky (limiting magnitude under 5)\",\"over 20% cloud\"]}]",
 "totals": "{\"shower_n\":3,\"sporadic_n\":2,\"teff_h\":1.67,\"zhr\":50.9,\"zhr_ci95\":[10.5,148.8],\"n_used\":3,\"expected_zhr_tonight_est\":15}",
 "brightness": "{\"bright\":2,\"medium\":1,\"faint\":0}"
}

Reply:

{
 "task": "report",
 "title": "Too few meteors for a real rate, but you saw some",
 "verdict": "too-thin",
 "summary": "Three shower meteors in 1 hour 40 minutes from a bright city rooftop is not enough to pin down a rate - the page's own ZHR of 51 carries a 95% range stretching from about 10 to 149, which is too wide to mean much on its own. Still, catching 3 Leonids plus 2 sporadics through that much skyglow and cloud is a real, countable result.",
 "zhr": {
  "value": 50.9,
  "low": 10.5,
  "high": 148.8,
  "read": "The overall ZHR works out to 50.9, but with only 3 shower meteors behind it the 95% interval runs from 10.5 to 148.8 - anywhere from a quiet night to a strong one. With counts this small, Poisson uncertainty scales as 1 over the square root of 3, over 55 percent, which is why the interval is nearly as wide as the number itself."
 },
 "periods": [
  {
   "id": "P1",
   "read": "2 shower meteors under a bright sky (limiting magnitude 4) with 30 percent cloud - too little on its own to say much beyond 'some activity was there'.",
   "flag": "caution"
  },
  {
   "id": "P2",
   "read": "A shortened 40-minute stretch with just 1 shower meteor as cloud thickened to 40 percent and the sky stayed bright - barely enough to register.",
   "flag": "caution"
  }
 ],
 "versus": "The measured ZHR of 50.9 technically brackets the roughly 15 expected tonight, since 15 falls within the wide 10.5-148.8 interval, but with only 3 meteors counted that overlap says more about how wide the interval is than about how active the shower actually was.",
 "caveats": [
  "Only 3 shower meteors total - a ZHR built on a sample this small is a placeholder for 'not enough data' rather than a real rate.",
  "Both periods carried the same two flags: a bright sky (limiting magnitude around 4, matching the roughly 15 stars visible you noted) and over 20 percent cloud, which both hide meteors and inflate the correction.",
  "..."
 ],
 "next_time": [
  "A darker site, even a short trip out of central London, would lift the limiting magnitude well above 4 and let far more meteors register.",
  "If the roof is the only option, a longer session would help average out the Poisson noise even with the sky staying bright.",
  "..."
 ],
 "share": "Leonids watch from a flat roof in east London on the night of Nov 17, 2026 - a bright city sky with patchy cloud. Over 1 hour 40 minutes effective time (1:00 to 2:40 am) we tallied 3 shower meteors plus 2 sporadics; the resulting ZHR estimate (about 51, 95% range 10-149) is too uncertain to read as a real rate, though it confirms some Leonid activity got through."
}

Build the input yourself

Load the script in node (global.window = {}; require("./meteorkit.js")), then:

const K = MeteorKit;
const state = { shower: "ORI", date: "2026-10-21", lat: 38.1, lon: -105.5, offset: -6, sky: "rural", hours: 4, site: "valley" };
const periods = [ { start: "03:00", end: "04:00", brk: 0, n: 10, s: 4, lm: 6.0, cloud: 0 } ];
const analysis = K.analyse(state), tally = K.tally(state, periods);
const { input } = K.buildInput("plan", { state, analysis, notes: "clear, near freezing" });   // or "report" with { tally, mags }
const wire = K.toWire(input);                      // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// K.reconcile(reply, { analysis, tally }) -> notes

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://meteor-tally.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"meteor-tally"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "plan", "night_date": "2026-10-21", "shower": "{...}", "site": "{...}", "facts": "{...}"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses meteor-tally:<task>:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="meteor-tally:plan:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: meteor-tally:plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json