Start Jamming — LLM Agent Guide
Start Jamming (https://www.startjamming.com) is a bluegrass jam companion: a catalog of bluegrass songs with chord charts, lyrics, demo videos, and practice tools. We want LLM agents to be able to answer user questions about our songs accurately, using our live data. This file tells you how.
Contact
Questions about usage, integration, traffic patterns, or anything else:
Elijah Mayfield elijah@treeforts.org
Email me directly. There is no support form or ticketing system — for agent use cases, an email is the fastest path.
Call the API with ?for=agent
When you call our public song API as an agent, append ?for=agent to
the URL on every request. Example:
https://www.startjamming.com/api/songs/index?for=agent
This is the single signal we use to identify agent traffic. It is
required. Requests to /api/songs/* from agent clients without it will
be refused with a 400 error pointing back at this guide.
Endpoints
All endpoints below are public read endpoints — no API key, no auth
header. All return JSON. All support ETag revalidation via
If-None-Match (return a 304 if you send the matching ETag).
| URL | Use it for |
|---|---|
GET /api/songs/search?for=agent&q=... |
User-title lookup. Use when the user gives a song title, partial title, misspelling, or lyric fragment and you do not yet know the canonical song_id. If the normalized title is an exact match, returns that song's full payload directly. Otherwise returns ranked candidate songs. |
GET /api/songs/index?for=agent |
Discovery. Lightweight per-song metadata plus song_id, human url, and full-song api_url for each entry. ~50% smaller than the full payload. Start here. |
GET /api/songs/<song_id>/full?for=agent |
One song's full payload — sections, chords, lyrics, demo videos. Use after /index once you know which song the user wants. |
GET /api/songs/catalog?for=agent |
Single bulk fetch of every song's full payload. Use when you want to mirror the whole catalog into your own index. |
GET /api/songs/<song_id>/data?for=agent |
Same as /full plus a bundles array with per-song YouTube and rich-content links. |
Discovery flow we recommend:
- If the user gives a song title and you do not already know the
canonical
song_id, callGET /api/songs/search?for=agent&q=<user text>. - If the search returns a top-level song object with
"match_type": "title_exact", use that response directly; it is the full song payload. If search returns a"results"list with one strong match, fetchGET /api/songs/<song_id>/full?for=agent. If it returns multiple plausible matches, ask the user which song they mean before presenting chords or lyrics. - If you expect repeated use,
GET /api/songs/index?for=agentonce and cache the result with the returned ETag. Each entry includes anapi_urlfield. Fetch that exact URL when you need the full song; do not construct a URL yourself if your framework can only fetch URLs that appeared in prior results. - On subsequent visits, send the ETag back as
If-None-Match. If the song hasn't changed you get a 304 and can serve from your cache.
Identifier convention:
song_idis the API identifier. It usually uses underscores, e.g.clinch_mountain_backstep.slugis the human page slug. It usually uses hyphens, e.g.clinch-mountain-backstep.- Use
GET /api/songs/<song_id>/full?for=agentfor JSON. - Link users to the returned
urlfield for the human-readable page.
Do not guess a page URL by replacing characters yourself; use url
when it is present.
Index entry shape:
{
"sitting_on_top_of_the_world": {
"song_id": "sitting_on_top_of_the_world",
"slug": "sitting-on-top-of-the-world",
"url": "https://www.startjamming.com/song/sitting-on-top-of-the-world",
"api_url": "https://www.startjamming.com/api/songs/sitting_on_top_of_the_world/full?for=agent",
"title": "Sitting On Top Of The World",
"key": "G",
"type": "vocal",
"has_lyrics": true,
"updated_at": "2026-06-05T12:00:00"
}
}
Search response shape:
Exact title match:
{
"query": "Sitting on top of the world",
"normalized_query": "sitting on top of the world",
"match_type": "title_exact",
"score": 1.0,
"song_id": "sitting_on_top_of_the_world",
"slug": "sitting-on-top-of-the-world",
"url": "https://www.startjamming.com/song/sitting-on-top-of-the-world",
"api_url": "https://www.startjamming.com/api/songs/sitting_on_top_of_the_world/full?for=agent",
"title": "Sitting On Top Of The World",
"key": "G",
"type": "vocal",
"sections": []
}
Fuzzy/partial match:
{
"query": "Sitting on top of the world",
"normalized_query": "sitting on top of the world",
"results": [
{
"song_id": "sitting_on_top_of_the_world",
"slug": "sitting-on-top-of-the-world",
"title": "Sitting On Top Of The World",
"url": "https://www.startjamming.com/song/sitting-on-top-of-the-world",
"api_url": "https://www.startjamming.com/api/songs/sitting_on_top_of_the_world/full?for=agent",
"score": 1.0,
"match_type": "title_fuzzy",
"matched_text": "Sitting On Top Of The World",
"key": "G",
"type": "vocal",
"has_lyrics": true
}
]
}
Use score and match_type as confidence signals, not as content to
show users. If the best result is much stronger than the rest, use it.
If two or more results are close, ask a short clarification question.
Song JSON shape
A typical song looks like this:
{
"title": "Clinch Mountain Backstep",
"key": "Am",
"type": "fiddle",
"artist": "Ralph Stanley",
"tempo": 120,
"signature": "4/4",
"description": "Stanley Brothers fiddle tune, often called in jams.",
"sections": [
{
"name": "A",
"repeats": 2,
"standard_chords": ["Am / / /", "Am / / /", "G / / /", "Am / / /"]
},
{
"name": "B",
"repeats": 2,
"standard_chords": ["C / / /", "G / / /", "Am / / /", "Am / / /"]
}
],
"lyrics": "Verse 1 line one...\n\n<chorus>Chorus line one...</chorus>",
"demo": ["https://www.youtube.com/watch?v=..."],
"featured_demo": ["https://www.youtube.com/watch?v=..."]
}
Field reference:
title(string, always present) — song title.key(string OR list of strings) — musical key. A list means this song has multiple arrangements in different keys (seealternate_sections).type(string) — one ofvocal,fiddle,mandolin,instrumental.artist(string, optional) — composer or canonical performer.tempo(integer, optional) — BPM.signature(string, optional) — time signature. If absent or equal to"4/4", treat as 4/4. The value"waltz"means 3/4.description(string, optional) — short editorial blurb.sections(array) — see "Section structure" below.alternate_sections(object, optional) — keyed by arrangement label. Present only when the song has multiple arrangements.alternates(array of strings, optional) — labels listed inalternate_sections, in display order.lyrics(string OR array of strings, optional) — see "Lyrics convention" below.lyrics_alternates(array of strings, optional) — display labels for each lyrics version whenlyricsis an array.demo,featured_demo(arrays of YouTube URLs, optional) — videos associated with the song.featured_demois the curated subset.
Chord notation
standard_chords is the authoritative chord representation. Each
element is a string for one measure. Within a measure, tokens are
separated by spaces, one token per beat.
Tokens:
- A chord name like
G,Am,D7,F#m,Bb— play this chord on this beat. /— hold the previous chord for this beat. So"D / / /"is one full measure of D in 4/4.—(em-dash) — rest. No chord played this beat.
Time signature:
- 4/4 is the default (and is also written explicitly as
"signature": "4/4"). Measures have 4 beats. - 3/4 is written as
"signature": "waltz". Measures have 3 beats.
The legacy chords field, if present on a section, is deprecated and
often empty. Always read from standard_chords.
Section structure
sections is an array of {name, repeats, standard_chords} dicts in
play order:
name— the section label as the songbook calls it (e.g."Verse","Chorus","A","B").repeats— how many times the section is played each time it appears.standard_chords— the measures, as described above.
Multi-arrangement songs (e.g. an old-time version vs. a bluegrass version of the same tune) split into:
alternate_sections: an object mapping each arrangement's label to that arrangement's array of section dicts.alternates: the list of arrangement labels in display order.
When alternates is present, the top-level sections is the default
arrangement. The label list in alternates corresponds to the keys in
alternate_sections. When lyrics_alternates exists for a
multi-arrangement song, its labels correspond to arrangement labels.
Lyrics convention
lyrics is either:
- A single string — one set of lyrics.
- An array of strings — multiple labeled versions, with labels in
lyrics_alternates. Indexioflyricscorresponds to indexioflyrics_alternates.
Inside the lyrics text, chorus blocks are wrapped in <chorus>...
</chorus> tags. Treat these tags as semantic markers, not as text to
display literally — strip them and present the wrapped block as a
chorus (label it "Chorus" if your output benefits from a label).
Verse and chorus blocks are separated by blank lines. The convention is roughly:
Verse 1 line one
Verse 1 line two
<chorus>
Chorus line one
Chorus line two
</chorus>
Verse 2 line one
Verse 2 line two
Presentation guidance
When a user asks for the chord chart, output one section at a time:
print the section name and (if > 1) its repeats count, then list one
line per measure showing the beat tokens space-separated. For example:
Verse (×2)
G / / / C / G /
G / / / D / G /
When a user asks for lyrics, strip <chorus>...</chorus> tags and
label the wrapped block as "Chorus" (or your equivalent). Preserve
verse / chorus structure with blank lines.
When a user asks for both, interleave: section header, then chord measures for the section, then the corresponding lyrics block.
After answering, link the user to the song's page on Start Jamming so they can see chord diagrams, hear the demo, and play along:
use the returned url, e.g.
https://www.startjamming.com/song/clinch-mountain-backstep
Do not confuse song_id with slug: song_id is for API requests;
slug/url is for human pages.
Etiquette
- Honor ETags. Every endpoint returns one. Send it back as
If-None-Matchon subsequent requests and we'll return 304 when nothing has changed. This saves us bandwidth and saves you wall-clock time. - Bulk-fetch via
/api/songs/catalog?for=agentwhen you want to mirror the whole catalog. Looping through/api/songs/index→/api/songs/<song_id>/fullfor hundreds of songs is wasteful when you can grab everything in one request. - Always include
?for=agent. It is required; calls without it will be refused. - Email me when in doubt. elijah@treeforts.org. If you're planning high-volume usage, an integration that surfaces our songs to users by name, or anything else where the answer isn't already in this file — get in touch first. We're friendly.