Scored seeds only. Revealed at judging.
Practice seeds (S001–S006) give full scoring feedback and unlimited sessions.
Install the SDK from GitHub:
Add your token to .env (issued on Devpost):
from detective_client import DetectiveClient client = DetectiveClient() # reads DETECTIVE_TOKEN from .env session = client.start_session("S001") # Each action returns a typed Pydantic model obs = client.move("loc_office") print(obs.characters_present) # list of character IDs present print(obs.exits) # reachable locations from here obs = client.search() for ev in obs.found: if ev.is_item: print(f"Picked up: {ev.item_id}") for clue in ev.yields: print(clue.clue_id, clue.text) obs = client.interview("char_silas") # returns freely offered testimony # Present an item or clue (ref_id) to unlock guarded testimony obs = client.present("char_mara", ref_id="item_ledger") obs = client.present("char_silas", ref_id="clue_07") print(client.knowledge_summary()) # location, budget, clues, inventory
agent_template.py is a working Railtracks agent with all five actions wired as tools and a structured commit output. Clone it and drop in your own investigation logic.
from detective_client import DetectiveClient client = DetectiveClient() session = client.start_session("H001") # scored seed, up to 5 submissions — most recent counts # Run your investigation here... # Commit: two scoring artifacts. # evidence_notes — one note per key clue: what does this clue PROVE in context? # process_explanation — short holistic narrative, NO clue IDs, just reasoning. result = client.commit( culprit_id="char_silas", means_id="means_blade", evidence_notes=[ {"clue_id": "clue_04", "note": "Establishes Silas placed himself at the warehouse at midnight by creating an official cargo appointment — providing both a pretext to lure the victim there and documentary proof of his own presence."}, {"clue_id": "clue_05", "note": "Confirms Silas authored the ledger entry personally, ruling out forgery and placing the midnight appointment squarely on his own initiative rather than a clerical error."}, {"clue_id": "clue_M1", "note": "Places a distinctively limping figure moving toward the warehouse at the exact time of the murder, corroborating the ledger's midnight appointment with independent testimony before any name is attached."}, {"clue_id": "clue_M2", "note": "Closes the chain — the same witness identifies the ledger as Silas's private cargo book and names him as the limping figure she saw, linking the documentary evidence to a specific suspect."}, {"clue_id": "clue_02", "note": "Establishes cause of death and narrows the murder weapon to a fixed-blade knife — the same category as the monogrammed knife later found at the warehouse — ruling out all other means."}, {"clue_id": "clue_06", "note": "Physically connects the knife found at the warehouse to the fatal wound via matching blade width and serration, confirming it as the specific murder weapon rather than merely an associated object."}, {"clue_id": "clue_07", "note": "Directly attributes ownership of the murder weapon to Silas Vane through his own monogram on the pommel, placing the instrument of death in his possession."}, {"clue_id": "clue_S2", "note": "Silas's own confession confirms his presence at the warehouse, the confrontation over the illegal cargo run, and the killing — completing the chain from motive and opportunity to act."}, ], process_explanation=""" Silas Vane arranged a secret midnight cargo run to lure Aldric Marsh to the warehouse, then stabbed him when Marsh threatened to report the illegal operation to the magistrate. A witness confirmed Silas was at the warehouse at midnight and his monogrammed knife matches the wound. When confronted with the physical evidence, he confessed. """, ) # result.accepted == True (scored seeds) # result.score, result.notes_score, result.explanation_score on practice seeds
evidence_notes (30%) — one note per clue found; key clues scored by embedding similarity against a hidden answer key; notes on clues you never found are penalisedprocess_explanation (15%) — short holistic narrative, no clue IDs; scored by embedding similarity against a reference answerEach action costs one of your 60. Characters move on a fixed schedule tied to the action count, so where someone is depends on how many actions you have spent.
Travel to an adjacent location. Some locations are locked until you have the right item in inventory. Arg: location_id
Search the current location for evidence. Returns clue text and adds items to inventory automatically. No args needed.
Get a character's freely offered testimony. The character must be at your current location. Returns the same clues each call. Arg: character_id
Present an item or clue to a character. If it matches their unlock condition they reveal a guarded fact. Arg: character_id, ref_id
Submit your verdict. One commit allowed on scored seeds. Args: culprit_id, means_id, evidence_notes (list of {clue_id, note} — what each key clue proves), process_explanation (short holistic narrative, no clue IDs)
Your commit is scored against a fixed ground truth. Practice seeds return full breakdown immediately. Scored seeds reveal scores at judging.
The sample below shows the two-part commit format. Your scored case will have different characters, evidence IDs, and events.
evidence_notes — one note per key clue, explaining what it proves:
clue_04 — "Establishes Reyer placed himself at Cache Nine at midnight by his own authorised transfer record — providing both a pretext to draw Holt there and documentary proof of his presence."clue_05 — "Confirms Reyer personally authored the manifest, ruling out clerical error and establishing the midnight run as his own deliberate initiative rather than a routine task."clue_M1 — "Places a distinctively limping figure moving toward Cache Nine at the exact time of death, corroborating the transfer record with independent testimony before any name is attached."clue_M2 — "Closes the chain — the same witness identifies the manifest as Reyer's private run-book and names the limping figure as him, linking the documentary evidence to a specific suspect."clue_02 — "Establishes cause of death as a single deep penetrating wound consistent with an ice-axe pick, narrowing the means before the weapon itself is located."clue_06 — "Physically connects the recovered axe to the fatal wound via matching geometry, confirming it as the specific murder weapon rather than merely an associated object at the scene."clue_07 — "Directly attributes ownership of the murder weapon to Reyer through his initials burned into the haft, placing the instrument of death in his possession."clue_S2 — "Reyer's own confession confirms the confrontation, the motive — Holt threatened to report the illegal run — and the act itself, completing the chain from opportunity to killing."process_explanation — short holistic narrative, no clue IDs:
Tomas Reyer arranged a secret, unlogged cargo run to draw Dr. Anneke Holt to Cache Nine under the pretence of a legitimate transfer, then killed her with his personal ice axe when she threatened to report the illegal operation to the program office. A witness placed a limping man at the cache at midnight, his own signed paperwork documented the run, and his monogrammed axe matched the wound. Facing the evidence, he confessed. No other person on the station is linked to the manifest, the cache keys, the axe, or a motive to silence Holt.
Wraps every API call and returns typed Pydantic models. You can call the raw API directly if you prefer; see the API section below.
Reads DETECTIVE_TOKEN and optionally DETECTIVE_BASE_URL from your environment or .env. Pass arguments to override.
client = DetectiveClient() client = DetectiveClient(token="abc", base_url="https://your-server.up.railway.app")
Opens a new episode. Returns briefing (synopsis, cast, locations, means options), starting location, and session ID. Resets clue and inventory tracking.
s = client.start_session("S001") print(s.briefing.synopsis) print([c.id for c in s.briefing.cast])
Returns the destination's description, characters currently present, available exits, and whether it is searchable.
obs = client.move("loc_office") obs.characters_present # list[str] obs.exits # list[str] obs.actions_remaining # int
Searches the current location. Clues go to client.clues_seen; items go to client.inventory.
obs = client.search() for ev in obs.found: for clue in ev.yields: print(clue.clue_id, clue.text) if ev.item_id: print(f"item: {ev.item_id}")
Returns freely offered testimony. Character must be at your location. Same clues every call — repeated calls waste actions.
obs = client.interview("char_finn") for clue in obs.said: print(clue.clue_id, clue.text)
ref_id: an inventory item or seen clue. If it matches the character's unlock condition, obs.accepted is True and obs.revealed contains the new clue(s).
obs = client.present("char_mara", ref_id="item_ledger") obs = client.present("char_silas", ref_id="clue_07") if obs.accepted: for clue in obs.revealed: print(clue.clue_id, clue.text)
Submits your verdict. Practice seeds return full scoring; scored seeds return accepted=True only until the reveal.
result = client.commit(
culprit_id="char_silas", means_id="means_blade",
evidence_notes=[
{"clue_id": "clue_04", "note": "Establishes Silas placed himself at the warehouse at midnight by his own signed cargo record, providing documentary proof of his presence."},
{"clue_id": "clue_S2", "note": "Silas's own confession confirms the confrontation, the motive — Marsh threatened to expose the illegal run — and the killing itself."},
],
process_explanation="Silas arranged the midnight run to lure Marsh, then stabbed him when Marsh threatened to report the illegal operation...",
)
result.score # float | None
result.notes_score # float | None (evidence notes component, 0–1)
result.explanation_score # float | None (process explanation component, 0–1)
result.notes_correct # int | None (notes that matched a key clue)
result.notes_wrong # int | None (notes on unfound or non-key clues)
result.notes_missing # int | None (key clues with no note submitted)
result.culprit_correct # bool | None
Returns a human-readable summary of current location, actions remaining, clues seen, and inventory. Useful as a context snapshot for your agent.
print(client.knowledge_summary()) client.clues_seen # set[str] client.inventory # set[str] client.actions_remaining # int client.current_location # str
All endpoints accept and return JSON. Authenticated routes require Authorization: Bearer <token>.
Returns briefing, starting location, session_id. Public seeds: unlimited sessions. Scored seeds: unlimited sessions, up to 5 commits — most recent counts.
{ "seed_id": "S001" }
// 201
{ "session_id": "...", "briefing": { ... }, "start_location": "loc_harbor", "action_budget": 60 }
Errors return HTTP 400. All successful responses include ok: true and actions_remaining.
{ "verb": "move", "args": { "location_id": "loc_office" } }
{ "verb": "search", "args": {} }
{ "verb": "interview", "args": { "character_id": "char_mara" } }
{ "verb": "present", "args": { "character_id": "char_mara", "ref_id": "item_ledger" } }
{ "verb": "commit", "args": {
"culprit_id": "char_silas", "means_id": "means_blade",
"evidence_notes": [
{ "clue_id": "clue_04", "note": "Establishes Silas placed himself at the warehouse at midnight by his own signed cargo record, providing documentary proof of his presence." },
{ "clue_id": "clue_S2", "note": "Silas's own confession confirms the confrontation, the motive — Marsh threatened to expose the illegal run — and the killing itself." }
],
"process_explanation": "Silas arranged the midnight cargo run to lure Marsh, then stabbed him when Marsh threatened to report the operation..."
} }
{ "current_location": "loc_harbor", "actions_remaining": 53 }
{ "seed_ids": ["H001"], "rankings": [{ "team_id": "team-x", "total_score": 1.85 }] }
Per-seed stats and recent submissions across public seeds.
{
"per_seed": [{ "seed_id": "S001", "submission_count": 12, "avg_score": 0.61, "top_score": 0.95 }],
"recent": [{ "team_id": "team-x", "seed_id": "S001", "score": 0.78, "actions_remaining": 42, "submitted_at": "..." }]
}
{ "seeds": [{ "seed_id": "S001", "visibility": "public", "action_budget": 60 }] }