API reference
Overview
The station’s API is free, read-only, unauthenticated JSON:API at https://radio.gridcoin.club/api. The Gridcoin chain is the archive: scores are compiled on demand from the frozen gridradio:v1 mapping. The server keeps the likes and plays ledgers and every block’s rendered audio. Anything the web player does, you can do: the score endpoints carry the notes, and the render endpoints carry the performance itself, the same file the station plays.
Versioning
Every score carries a spec tag (currently gridradio:v1). Spec versions activate by block height, like the chain’s own consensus rules: /status publishes the era table, and a block compiles under the spec of its era. A later spec can only govern blocks staked after its activation height, so a block’s score never changes once its era is fixed: cached responses never go stale and conformant players never retune history.
Service status
/statusHealth + station context
Version, network, current chain tip, what’s on air, and how many listeners are tuned in. Always answers 200 while the service is up, with height: null if the wallet daemon is unreachable.
{"data": {"type": "status","attributes": {"name": "Grid Radio","version": "1.0.0","network": "mainnet","spec": "gridradio:v1","specEras": [{ "spec": "gridradio:v1", "fromHeight": 0 }],"height": 3265828,"onAirHeight": 3265828,"listeners": 12,"targetSpacingSeconds": 90}}}
Now playing
/airCurrent block, score, weather, tracklist
The current broadcast: the block on air, its full compiled score, the network weather (staking difficulty and net stake weight), and the recent tracklist. Answers 503 Dead Air until the station has heard its first block after boot.
curl https://radio.gridcoin.club/api/air
{"data": {"id": "3300002","type": "air","attributes": {"height": 3300002,"blockHash": "1ac093a9abd55fe479227a6a17d485d5963c61e7d216361b1fc07c712fb24a7d","time": 1720261616,"txCount": 2,"difficulty": 22.06,"score": {"spec": "gridradio:v1","hash": "1ac093a9abd55fe479227a6a17d485d5963c61e7d216361b1fc07c712fb24a7d","title": "Brief Vector","key": "B","keySemitone": 2,"mode": { "id": 6, "name": "lydian", "mood": "aurora" },"tempo": 43,"steps": 64,"melody": [{ "step": 0, "semitone": 24, "velocity": 0.55, "length": 8 },{ "step": 8, "semitone": 24, "velocity": 0.57, "length": 8 },"… 6 more notes"],"bass": [{ "step": 0, "semitone": -12, "velocity": 0.55 },"… 7 more notes"],"bells": [],"drone": { "detuneCents": 7, "fifth": true },"piece": "chorale","voices": [{"role": "alto","notes": [{ "step": 0, "semitone": 7, "velocity": 0.48, "length": 8 },"… 7 more notes"]},{"role": "tenor","notes": [{ "step": 0, "semitone": 4, "velocity": 0.47, "length": 8 },"… 7 more notes"]}],"harmony": {"chords": [{ "root": 0, "bass": 0, "tones": [0, 4, 7] },{ "root": 2, "bass": 0, "tones": [2, 6, 9] }],"barSteps": 16,"feel": "slow","pedal": true}},"startedAt": "2024-07-06T10:27:03.512Z","likes": 0,"weather": {"difficulty": 22.06,"netStakeWeight": 8690146724.1,"observedAt": "2024-07-06T10:27:41.908Z"},"recent": [{"height": 3300002,"blockHash": "1ac093a9…","time": 1720261616,"txCount": 2,"title": "Brief Vector","key": "B","mode": "lydian","mood": "aurora","tempo": 43},{"height": 3300001,"blockHash": "e6c6f193…","time": 1720261568,"txCount": 2,"title": "Drifting along the Frozen Neutrino","key": "B","mode": "mixolydian","mood": "amber","tempo": 63},"… up to 10 more"]}}}
Every score carries piece (one of band, solo-piano, drone-and-line, tintinnabuli, canon, chorale, process, broken-chords) and harmony: each chord spelled as its root, what the floor holds under it and its tones, all in semitones above the key, plus the steps between chord changes, the pace, and whether the floor pedals on the tonic. Pieces with more than one written line carry voices, parallel lines with a role and, for a canon’s followers, the step they first enter on. A process piece carries process: the cell and the rule the player renders every pass from, the score itself holding only the first pass. Notes of the sustained pieces carry a length in steps; a note without one holds to the next onset.
Replay the archive
/blocks/:idAny block, by height or hash
Every block since 2013, by height or 64-hex block hash: block metadata (time, transactions, difficulty, confirmations, likes, plays) plus its compiled score. Deeply buried blocks are served with long cache lifetimes. Blocks past the tip answer 404, because the chain has not written them yet.
curl https://radio.gridcoin.club/api/blocks/1000000
{"data": {"id": "1000000","type": "blocks","attributes": {"height": 1000000,"blockHash": "10234b91c09e9121829aa5f76eaec2d7e6d42b5cd550727e27c1226fc71eeb8a","time": 1503316176,"txCount": 2,"difficulty": 38.1,"confirmations": 3099209,"likes": 0,"plays": 0,"score": {"spec": "gridradio:v1","hash": "10234b91c09e9121829aa5f76eaec2d7e6d42b5cd550727e27c1226fc71eeb8a","title": "The Mild and Sober Spectrum Rises","key": "C#","keySemitone": 4,"mode": { "id": 0, "name": "major pentatonic", "mood": "clear" },"tempo": 40,"steps": 64,"melody": [{ "step": 0, "semitone": 9, "velocity": 0.57 },{ "step": 1, "semitone": 16, "velocity": 0.46 },"… 62 more notes"],"bass": [{ "step": 0, "semitone": -3, "velocity": 0.55 },"… 3 more notes"],"bells": [],"drone": { "detuneCents": 3, "fifth": true },"piece": "broken-chords","harmony": {"chords": [{ "root": 9, "bass": 9, "tones": [9, 14, 16] },{ "root": 0, "bass": 0, "tones": [0, 4, 7] }],"barSteps": 16,"feel": "slow","pedal": false}}}}}
Like a block
/blocks/:id/likesOne like per listener per block
/blocks/:id/likesChange your mind
One of the station’s two writes. There is no body and no auth; the block id (height or hash) is the whole request. One like per listener per block per day; a repeat answers 200 with counted: false rather than an error, because loving something twice is not a client mistake. DELETE takes a like back, but only one the server remembers you giving, so nobody can drain a block they did not love.
{"data": {"id": "10234b91c09e9121829aa5f76eaec2d7e6d42b5cd550727e27c1226fc71eeb8a","type": "likes","attributes": {"height": 1000000,"likes": 1,"counted": true}}}
Count a play
/blocks/:id/playsA block went on your air
The other write: the attendance ledger. If you build a player on this API, report each block as it starts and the archive remembers it was listened to. Same manners as the likes: no body, no auth, one counted play per listener per block per day, counted: false on repeats. The total comes back on GET /blocks/:id as plays. There is no DELETE; a tune that has been heard has been heard.
Listen to a block
/blocks/:id/renderIs it rendered, and where are the files
/blocks/:id/renderRender it
/blocks/:id/streamHear it while it renders
The station performs every block once and keeps the result: an MP3 of the block’s whole variation cycle, plus a visual script with what the sky and the dial read, frame by frame. Every listener gets the same two files. A new block is rendered as soon as it is staked. For any other block, POST asks for it (202) and GET reports none, queued, rendering or ready; a cold block takes about half a minute. Once it is ready, the response names both files. Their names carry what made them, so they never change and cache forever.
{"data": {"type": "renders","id": "<block hash>","attributes": {"height": 1000000,"status": "ready","position": null,"audio": "/renders/<engine>/<block hash>-<score>.mp3","visual": "/renders/<engine>/<block hash>-<score>.json"}}}
The file is one pass cycle plus about eight seconds of reverb tail. To loop it the way the station does, start the next copy at the visual script’s musicSeconds, not at the end of the file, so the tail rings over the next start. The script also lists the bar lines (to hand over to another block on one), the bass, melody and bell strikes, and 60 spectrum frames a second.
A cold block does not have to be waited for: GET /blocks/:id/stream starts its render and sends it as it is made, the first audio about a second in. The body is a run of frames, each a one-byte type, a four-byte little-endian length and the payload: type 1 is the visual script without its spectrum (JSON); type 2 is a chunk, four little-endian 32-bit integers (first frame, frame count, first spectrum frame, spectrum byte count) then 16-bit interleaved stereo at 48 kHz and the spectrum bytes; type 3 ends it (JSON, with the finished files). An already rendered block sends only the end. Hang up and a render nobody else wants is dropped.
The charts
/chartsMost loved tunes, most likes first
Blocks ranked by likes, with their titles, keys and moods compiled from the hash on the way out. ?limit=N up to 100, default 25.
{"data": [{"id": "10234b91c09e9121829aa5f76eaec2d7e6d42b5cd550727e27c1226fc71eeb8a","type": "charts","attributes": {"blockHash": "10234b91c09e9121829aa5f76eaec2d7e6d42b5cd550727e27c1226fc71eeb8a","height": 1000000,"likes": 1,"title": "The Mild and Sober Spectrum Rises","key": "C#","mode": "major pentatonic","mood": "clear","tempo": 40}},{ "…": "one more" }]}
The performance layer
Every endpoint above returns the score: notes, key, mode, tempo, form, title, piece, chords. None of them tells you which instrument plays the melody, how the drone is built, or whether the block is performed outdoors. That is a second mapping, computed from the same 32-byte hash when the station renders the block, and frozen exactly the way the score is.
You do not need it to hear the station: the render endpoint hands you the performed block. It is a pure function of the hash, so a player of your own can reproduce it, ignore it and pick its own instruments, or do something else entirely. The notes will still be right either way.
casting 1 of 15 lead seats (13 recorded, 2 synthesised),drawn from the seats that can play the line;1 of 10 bass seats, 1 of 4 bell characters,1 of 12 floors, and whether each is present at alltimbre the block's own electric piano, plucked string,FM bell and granular floortuning equal temperament, a just floor under a temperedlead, or just intonation throughoutfeel swing, timing spread, lead lag, tempo breath, andwhere each player stands on the stagearrangement whether the band walks on stage, and in what orderarp figure how a piano-arpeggio drone phrases itself: how fast,and how much of each chord window it fills before restingweather about 1 droned block in 5 swaps its drone for one of 82CC0 field recordings, at a level and a filter cutoff thehash picks; the cutoff reads as distanceeffects delay time and depth, reverb room sizepasses the variation cycle, 4 or 6 passes longsky colours, motion, 1 or 2 of 12 beat-synced visualeffects, 1 of 6 dial kinds, and what hangs insidethe dial
Both mappings are versioned by the same tag: the score’s spec selects the performance rules too, so a cached response and a re-render years apart still agree.
Embed a tune
If all you want is a block’s tune on your own page, skip the API entirely: every block has an iframe-able mini player at /embed/:id, by height or hash. It plays the station’s own render of the block under the same sky, and it asks its visitors for nothing: no account, no cookies, no tracking beyond the station’s own anonymous counters.
<iframesrc="https://radio.gridcoin.club/embed/1000000"width="100%" height="152"style="border:0;border-radius:12px"title="Grid Radio"loading="lazy" allow="autoplay"></iframe>
Every block page also serves a share card at /og/block/:id: the tune’s title, sky and dial as a 1200×630 PNG, deterministic like everything else. Link a block anywhere that unfurls previews and the card comes along on its own.
Errors & limits
Errors follow JSON:API conventions: 404 for blocks that do not exist, 470 for malformed parameters, 503 for dead air (no block heard yet) or a renderer with a full queue (with a Retry-After). Reads are rate-limited per IP as an abuse valve, not a budget; the limits are far above anything a player needs. Asking for a render is the one costly request, so it gets a few a minute. Be a good listener: poll gently, and cache deeply buried /blocks responses, because they never change.
Made with ❤ by @gridcat · Part of Gridcoin Club ↗