Your first mod
This guide takes you from nothing to a working mod running inside PolyTrack. No prior modding experience needed. If you can write a little JavaScript, you can write a mod.
The shortest possible mod
A TSPML mod is, at minimum, two pasted files. Here's a complete, working mod you can try right now at tspml.vercel.app:
mod.json
{
"schemaVersion": 1,
"id": "hello-polytrack",
"name": "Hello PolyTrack",
"version": "1.0.0",
"entrypoint": "entrypoint.js",
"targets": [">=0.6.0 <0.7.0"]
}entrypoint.js
export default (api) => {
api.events.on('checkpoint.passed', ({ index, isReplay }) => {
if (isReplay === true) return; // skip ghost cars
api.logger.log(`[hello] checkpoint ${index} passed!`);
});
api.keybinds.register({
id: 'hello-polytrack.greet',
key: 'KeyH',
description: 'Say hello',
onDown: () => api.logger.log('[hello] Hello from my mod!'),
});
api.logger.log('[hello] loaded');
// Return a disposer; called when the mod is disabled/removed.
return () => api.logger.log('[hello] unloaded');
};Open the portal, expand Add a mod, paste the two files into the first two boxes, click Add mod. The sidebar shows mods: ✓ hello-polytrack, checkpoints log as you race, and H greets you.
That's the whole loop. Everything below is refinement.
The entrypoint contract
Your entrypoint is an ES module whose default export is a factory:
export default (api) => {
// set up: subscribe, register…
return () => { /* tear down */ };
};Three rules worth internalizing:
Everything you register returns an undo.
api.events.on(...)returns an unsubscribe function;api.keybinds.register(...)returns an unregister function. Call them in your disposer:jsexport default (api) => { const off = api.events.on('car.control', handler); const unbind = api.keybinds.register({ ... }); return () => { off(); unbind(); }; };Returning nothing is fine. Your mod is just reported as
no-opon unload instead ofunloaded.Throwing is contained. A mod that throws, whether at load, in an event handler, or during unload, is caught, reported in the sidebar, and never takes down the game or other mods.
Developing with TypeScript (optional)
If you prefer TypeScript and a real project folder, scaffold one from the TSPML repo:
git clone https://github.com/roowus/TSPML.git
cd TSPML && pnpm install --ignore-scripts
node tooling/create-tspml-mod/bin/create-tspml-mod.mjs my-first-mod
cd ../my-first-mod && pnpm install && pnpm buildYou get a typed src/entrypoint.ts (with a local TspmlApi type declaration), a starter mixins.json, and a self-contained tsconfig. pnpm build emits dist/src/entrypoint.js, and that built file is what you paste into the portal, not your TypeScript source.
Why paste the built file?
The portal imports your entrypoint as a real ES module via a Blob URL. Browsers execute JavaScript, not TypeScript, so the compiled output is the mod.
Things every mod should know
Ghost cars fire events too
race.started, checkpoint.passed, and race.finished fire once per car, and a ghost/replay car is a car. Every payload carries isReplay so you can tell them apart:
api.events.on('race.finished', ({ frames, isReplay }) => {
if (isReplay === true) return; // a ghost finished, not the player
showTime(frames);
});Compare with === true, not truthiness: isReplay is boolean | null, and null means TSPML could not determine it. Treat that as unknown, never as "the player".
Times are frames
race.finished reports frames, not milliseconds. PolyTrack simulates at 60 fps: seconds = frames / 60.
Iterating on your mod
Two loops, depending on how you work:
- Pasting: re-paste with the same
idand click Add mod again. The stored copy is replaced, the old instance unloads, the new one loads. Change, rebuild, re-paste. - Hosting: import your mod from a URL once, then push new builds to the same URL and click the reload button in the Your mods header. The portal re-fetches from the source and reloads. See Loading your mod.
Next steps
- Loading your mod: persistence, toggling, URL import, reloading, and the restart banner
- Events reference: every event with payloads and caveats
- Registries reference: keybinds, custom tracks, audio overrides
- Mixins: when the stable API isn't enough
- Publishing & sharing: hosting, share links, and the checklist