Adding a Tracker to the Registry¶
Adding a new tracker to an existing platform (UNIT3D, Gazelle, GGn, Nebulance, MAM, or AvistaZ) requires one file and two lines in the barrel export. No adapter code needed.
If your tracker runs on a new platform, read Tracker API Responses first — you'll need to write an adapter before adding the registry entry.
Standardization Philosophy¶
Every tracker file follows the same field order and completeness rules. This makes diffs clean and reviews fast.
Every field must be present, even if empty. Use "" for empty strings, [] for empty arrays, and false for booleans. Never omit fields or use undefined — if it's there, you've decided on it.
abbreviation: "" // not: abbreviation: undefined
logo: "" // not: logo: undefined
trackerHubSlug: "" // not: trackerHubSlug: undefined
bannedGroups: [] // not: bannedGroups: undefined
warning: false // not: warning: undefined
Three exceptions:
- Omit the
statsblock entirely when you don't have real data. Don't include it withundefinedvalues. rules.fulfillmentPeriodHours,rules.hnrBanLimit, andrules.fullRulesMarkdownare truly optional — omit them when unknown.- Platform-specific fields (
gazelleAuthStyle,gazelleEnrich,unit3dAuthStyle) only belong in tracker files for their own platform. Don't add them to other trackers.
1. Copy the Template¶
Copy src/data/trackers/_template.ts to a new file matching your tracker's slug (lowercase, hyphens only):
Full template:
// src/data/trackers/_template.ts
//
// Copy this file to add a new tracker to the registry.
//
// 1. Duplicate this file and rename it to your tracker's slug (e.g. mytracker.ts)
// 2. Fill in all fields below — every field must be present (use "" / [] / false
// rather than omitting). See inline comments for guidance.
// 3. Export from src/data/trackers/index.ts (add to the barrel + ALL_TRACKERS array)
// 4. Run `pnpm test` to validate your entry
//
// Set draft: true while the entry is incomplete. Draft trackers skip strict
// validation in CI, so you can submit a PR with partial data.
//
// Allowed content categories:
// Movies, TV, Music, Games, Apps, Sports, Books, Audiobooks, Comics,
// Manga, Anime, XXX, Documentaries, Education, Tutorials, Fanres,
// iOS Apps, Graphics, Audio
//
// Validator checks:
// - slug: lowercase letters and hyphens only
// - platform: "unit3d" | "gazelle" | "ggn" | "nebulance" | "mam" | "avistaz" | "digitalcore" | "btn" | "iptorrents" | "torrentleech" | "custom"
// - apiPath must match platform default:
// unit3d → "/api/user"
// gazelle → "/ajax.php"
// ggn → "/api.php"
// nebulance → "/api.php"
// mam → "/jsonLoad.php"
// - url: https only
// - contentCategories: values must come from the allowed list above
// - language: required
// - rules: required (minimumRatio, seedTimeHours, loginIntervalDays as numbers)
import type { TrackerRegistryEntry } from "@/data/tracker-registry"
export const mytracker: TrackerRegistryEntry = {
// ── Identity ────────────────────────────────────────────────────────
slug: "mytracker", // lowercase, hyphens only (e.g. "my-tracker")
name: "My Tracker", // display name
abbreviation: "", // short code (e.g. "ATH", "RED") — "" if none
url: "https://example.com", // base URL (https only)
description: "TODO", // 1-2 sentence overview
// ── Platform & API ──────────────────────────────────────────────────
platform: "unit3d", // "unit3d" | "gazelle" | "ggn" | "nebulance" | "mam" | "avistaz" | "digitalcore" | "btn" | "iptorrents" | "torrentleech" | "custom"
// Platform-specific fields (uncomment for your platform):
// gazelleAuthStyle: "token", // gazelle only — "token" | "raw"
// gazelleEnrich: true, // gazelle only — enables enrichment call
// unit3dAuthStyle: "bearer", // unit3d only — "bearer" | "query"
apiPath: "/api/user", // unit3d: "/api/user" | gazelle: "/ajax.php" | ggn: "/api.php" | mam: "/jsonLoad.php"
// ── Content ─────────────────────────────────────────────────────────
specialty: "", // what the tracker is known for (e.g. "HD Movies", "Anime")
contentCategories: [], // see allowed list in header
language: "English",
// ── Visual ──────────────────────────────────────────────────────────
color: "#000000", // hex accent color for the tracker's detail page
logo: "", // "/tracker-logos/mytracker_logo.svg" — file must exist in public/ — "" if none
// ── External Links ──────────────────────────────────────────────────
trackerHubSlug: "", // slug on trackerhub.xyz, if listed — "" if none
statusPageUrl: "", // external status page URL — "" if none
// ── Community ───────────────────────────────────────────────────────
userClasses: [], // [{ name: "Power User", requirements: "Upload ≥ 100 GiB" }]
releaseGroups: [], // [{ name: "GrpName", description: "Encodes" }] or ["GrpName"]
bannedGroups: [], // ["GroupName"] — groups explicitly banned by the tracker
notableMembers: [], // ["handle"] — notable community figures
// ── Rules ───────────────────────────────────────────────────────────
rules: {
minimumRatio: 0, // 0 = no minimum
seedTimeHours: 0, // 0 = no minimum
loginIntervalDays: 0, // 0 = no login interval policy
// fulfillmentPeriodHours: 72, // optional — hours to complete H&R seeding
// hnrBanLimit: 3, // optional — number of H&Rs before ban
// fullRulesMarkdown: `...`, // optional — detailed rules as markdown string
},
// ── Status ──────────────────────────────────────────────────────────
warning: false, // true if the tracker has a known issue or is at risk
warningNote: "", // short description of the warning — "" if none
// ── Flags ───────────────────────────────────────────────────────────
draft: true, // remove (or set false) once all required fields are filled in
supportsTransitPapers: false, // true if the tracker supports transit papers export
profileUrlPattern: "", // e.g. "/user.php?id={id}" — required when supportsTransitPapers: true
// ── Stats (omit this block entirely if no real data is available) ───
// stats: {
// userCount: undefined,
// activeUsers: undefined,
// torrentCount: undefined,
// seedSize: undefined, // e.g. "500 TiB"
// statsUpdatedAt: undefined, // ISO 8601 date string
// },
}
2. Field Reference¶
Fields are documented in template order, grouped by section.
Identity¶
slug¶
Type: string
The unique identifier for this tracker. Used in filenames, URLs, and database lookups. Lowercase with hyphens only.
name¶
Type: string
Human-readable display name. This is what users see in the UI.
abbreviation¶
Type: string
A short code for the tracker, used in compact UI contexts. Use "" if none.
url¶
Type: string
The base URL including protocol. HTTPS only, no trailing slash.
The adapter appends apiPath to this URL to make API requests.
apiPath is always a relative path beginning with / — this is enforced by
both the registry test and scripts/validate-trackers.ts. If a tracker serves
its API from a different host, that host belongs in its adapter as a module
constant, not in the registry; see btn.ts. Putting an absolute URL in
apiPath would persist it into every user's row with no migration path back.
description¶
Type: string
One or two sentences about what the tracker is — content focus, community reputation, anything someone might want to know before joining.
description: "The largest general music tracker (also has some software). Has an interview to join, although the wait can be notoriously long."
Platform & API¶
platform¶
Type: "unit3d" | "gazelle" | "ggn" | "nebulance" | "mam" | "avistaz" | "digitalcore" | "btn" | "iptorrents" | "torrentleech" | "custom"
Which adapter handles API requests. This tells the scheduler how to fetch stats. Pick the one that matches the tracker's software.
| Platform | What it means |
|---|---|
"unit3d" |
Runs UNIT3D |
"gazelle" |
Runs Gazelle or a fork (Orpheus, Gazelle-Music, etc.) |
"ggn" |
GazelleGames only — has its own custom API |
"nebulance" |
Uses Nebulance's API |
"mam" |
MyAnonaMouse — cookie-based auth via mam_id |
"avistaz" |
AvistaZ network — cookie auth + profile scraping |
"digitalcore" |
DigitalCore — custom JSON API, uid/pass session cookies |
"btn" |
BroadcasTheNet — JSON-RPC on a separate API host |
"iptorrents" |
IPTorrents — cookie auth + profile scraping |
"torrentleech" |
TorrentLeech — username/password login + profile scraping |
"custom" |
Placeholder, not implemented yet |
gazelleAuthStyle¶
Type: "token" | "raw" — Gazelle trackers only
Controls how the API token is sent in the request.
"token"— sends the token in anAuthorization: token TOKENheader (used by REDacted, Orpheus)"raw"— sends the token directly in theAuthorizationheader without a prefix
Only include this field for Gazelle trackers. If you are unsure which style a Gazelle tracker uses, check docs/kb/docs/contributing/tracker-responses-gazelle.md.
gazelleEnrich¶
Type: boolean — Gazelle trackers only
When true, the adapter makes a second API call (action=user&id=X) after the initial action=index call to get seeding/leeching counts, warned status, joined date, avatar, ranks, and community stats. Set this to true for all Gazelle trackers — without it, seeding and leeching will show as 0.
Only include this field for Gazelle trackers.
unit3dAuthStyle¶
Type: "bearer" | "query" — UNIT3D trackers only
Controls how the API token is sent in the request.
"bearer"— sends the token in anAuthorization: Bearer TOKENheader (required by UNIT3D v8+)"query"— sends the token as a?api_token=TOKENquery parameter (legacy UNIT3D)
Omit this field to use the default query parameter method. Set to "bearer" if the tracker's UNIT3D instance has been updated to v8+ and returns 401 with query param auth.
Only include this field for UNIT3D trackers.
apiPath¶
Type: string
The path appended to url for API requests. Must match the platform's actual endpoint.
| Platform | Default |
|---|---|
unit3d |
/api/user |
gazelle |
/ajax.php |
ggn |
/api.php |
nebulance |
/api.php |
avistaz |
/profile |
Almost all trackers use the platform default. Only change it if you've verified the tracker deviates.
AvistaZ trackers scrape HTML using browser cookies. All five AvistaZ sites (AvistaZ, AnimeZ, PrivateHD, CinemaZ, ExoticaZ) use the same adapter.
Content¶
specialty¶
Type: string
A short phrase describing what the tracker specializes in. Shown in the UI and used for filtering. Use "" if none.
contentCategories¶
Type: string[]
The categories of content hosted on the tracker. Must use values from the allowed list exactly as written (case-sensitive):
Movies, TV, Music, Games, Apps, Sports, Books, Audiobooks, Comics,
Manga, Anime, XXX, Documentaries, Education, Tutorials, Fanres,
iOS Apps, Graphics, Audio
language¶
Type: string
Primary language of the tracker. Use "English" for English-language trackers.
Visual¶
color¶
Type: string
A hex color code used to theme the tracker's detail page — chart colors, scrollbar, stat card accents. Pick something that represents the tracker's visual identity.
color: "#00d4ff" // Aither cyan
color: "#f44336" // REDacted red
color: "#7b1fa2" // GazelleGames purple
color: "#1a4fc2" // Nebulance blue
logo¶
Type: string
Path to the tracker's logo file in public/. Only set this if the file exists. Use "" if none. SVG preferred, PNG acceptable.
External Links¶
trackerHubSlug¶
Type: string
The tracker's slug on trackerhub.xyz, if the tracker is listed there. Used to link to its Trackerhub profile. Use "" if not listed.
statusPageUrl¶
Type: string
URL to an external status page. Many trackers have one at trackerstatus.info. Use "" if none.
Status¶
warning¶
Type: boolean
Set to true if there is something users should know before adding this tracker (for example, known API instability). Use false when there is no known issue.
warningNote¶
Type: string
Short description of the warning. Use "" if warning is false.
Flags¶
draft¶
Type: boolean
When true, the tracker is excluded from TRACKER_REGISTRY and skips strict validation in CI. Use this while you are filling in fields. Set to false (or remove the field) once the entry is complete.
supportsTransitPapers¶
Type: boolean
Set to true if the tracker supports the transit papers export feature. Use false otherwise.
profileUrlPattern¶
Type: string
The URL pattern used to construct a user's profile link. Required when supportsTransitPapers is true. Use "" if not applicable.
Stats¶
Omit the stats block entirely when you don't have real data. Don't include it with undefined values — the absence of the block signals that stats haven't been sourced yet.
When you have data, include only the fields you know:
stats: {
userCount: 12000,
torrentCount: 450000,
seedSize: "8.2 PiB",
statsUpdatedAt: "2025-09-01",
}
Available fields:
stats: {
userCount?: number
activeUsers?: number
torrentCount?: number
seedSize?: string // e.g. "500 TiB"
statsUpdatedAt?: string // ISO 8601 date string
}
3. User Classes¶
The userClasses array documents the tracker's rank/class system. Each entry is a TrackerUserClass:
interface TrackerUserClass {
name: string // required — display name of the class
requirements?: string // what it takes to reach this class
perks?: RankPerk[] // structured perks (optional, rarely populated)
icon?: string // path to an icon (optional)
}
For most trackers, name and requirements are enough. Write requirements as a human-readable summary — hit the key numeric thresholds (upload, ratio, account age, seed count) without being exhaustive.
// From aither.ts — upload-based progression
userClasses: [
{ name: "Leech", requirements: "Ratio below 0.4 — download privileges revoked" },
{ name: "Phobos", requirements: "Ratio ≥ 0.4. 4 download slots" },
{
name: "Zeus",
requirements:
"Upload ≥ 2 TiB or seed size ≥ 2 TiB, ratio ≥ 0.6, age ≥ 3 months, avg seedtime ≥ 10 days. 25 download slots",
},
// ...
// Staff-assigned ranks follow community ranks
{ name: "Uploader", requirements: "Staff — role model uploader. Freeleech, H&R immune" },
]
// From redacted.ts — simpler progression
userClasses: [
{ name: "User", requirements: "Default class on registration" },
{ name: "Member", requirements: "1 week, 25 GB up, 0.65 ratio" },
{ name: "Power User", requirements: "2 weeks, 25 GB up, 0.65 ratio, 5 torrents seeding" },
]
// From gazellegames.ts — achievement-point based
userClasses: [
{ name: "Amateur", requirements: "Default starting class" },
{
name: "Gamer",
requirements:
"600 achievement points. Invites, requests, collections, Top 10, peerlists, mass downloader",
},
]
If the tracker has no formal class system, leave the array empty:
The perks array accepts structured perk objects for when you want machine-readable perk data:
perks: [
{ type: "freeleech", label: "Freeleech on all torrents" },
{ type: "hnr-immune", label: "H&R immunity" },
{ type: "download-slots", label: "50 download slots" },
]
Valid RankPerkType values: "download-slots", "upload", "invite", "freeleech", "double-upload", "hnr-immune", "mod-bypass", "custom".
Structured perks are not required — a plain requirements string is enough for the UI to display the information.
4. Release Groups¶
The releaseGroups field accepts a mixed array — entries can be either a plain string or a ReleaseGroup object:
interface ReleaseGroup {
name: string
description?: string
}
releaseGroups: (string | ReleaseGroup)[]
Use the object form when you have something useful to say about what the group releases. Use a plain string when the name alone is sufficient (typically for banned groups, which belong in bannedGroups instead).
// From aither.ts — objects with descriptions
releaseGroups: [
{ name: "ATELiER", description: "Main house group — high-quality 1080p encodes and remuxes" },
{ name: "Kitsune", description: "Primary — WEB-DL" },
{
name: "MainFrame",
description: "Primary — 2160p encodes, secondary 1080p encodes and remuxes",
},
{ name: "ARTiCUN0" }, // object without description — name only
]
If the tracker has no notable internal groups, or you do not know them, use an empty array:
The bannedGroups field is a separate string[] for groups that are explicitly banned from the tracker:
5. Rules¶
The rules field documents the tracker's seeding and account policies. The type:
interface TrackerRules {
minimumRatio: number // required — 0 = no minimum
seedTimeHours: number // required — 0 = no minimum
loginIntervalDays: number // required — days before account is disabled; 0 = no policy
fulfillmentPeriodHours?: number // total hours allowed to complete H&R seeding
hnrBanLimit?: number // number of active H&Rs before downloading is blocked
fullRulesMarkdown?: string // the full rules as a markdown string
}
minimumRatio, seedTimeHours, and loginIntervalDays are required even if the tracker has no policy — use 0 to mean "not enforced."
// From nebulance.ts — ratioless tracker
rules: {
minimumRatio: 0, // ratioless
seedTimeHours: 72,
loginIntervalDays: 90,
}
// From aither.ts — full policy
rules: {
minimumRatio: 0.4,
seedTimeHours: 120,
loginIntervalDays: 90,
fulfillmentPeriodHours: 480, // 20 days to complete H&R seeding
hnrBanLimit: 3, // 3 active H&Rs → downloads blocked
fullRulesMarkdown: "...",
}
For fullRulesMarkdown, use array-join format. It keeps diffs clean and avoids multiline template literal indentation issues:
fullRulesMarkdown: [
"## Golden Rules",
"**1.** Do not defy the expressed wishes of the staff.",
"**2.** Access is a privilege, not a right.",
"",
"## Ratio System",
"Required ratio starts at 0.00 and rises as you download more.",
"",
"## Seeding Rules",
"- Torrents must be seeded for **72 hours** after snatching.",
].join("\n"),
6. Register in the Barrel File¶
Open src/data/trackers/index.ts and add in three places (alphabetized):
Export block:
Import block:
ALL_TRACKERS array:
The const name must match what you exported from your tracker file.
7. Verify It Works¶
Type check¶
This catches missing required fields or type mismatches. Fix all errors before proceeding.
Run the test suite¶
The test suite validates tracker registry entries: required fields, valid platform types, correct apiPath values per platform, valid content category names, and plain-string bannedGroups entries.
Check it in the UI¶
- Start the dev server:
pnpm dev - Go to
/trackers/new - Search for your tracker by name in the Add Tracker dialog
- Add it with a valid API token
- Click Poll Now on the tracker card
- Confirm the tracker status changes and stats appear in the dashboard
If the tracker appears in search results and polls successfully, the registry entry is correct.
8. Adding a Whole New Platform¶
Everything above covers adding a tracker to a platform that already has an adapter. If the tracker runs software we don't support yet, a registry entry alone is not enough — it will show up in the picker and then fail at poll time.
A new platform touches five places. Miss any one and the tracker is either invisible or unusable:
| # | What | Where |
|---|---|---|
| 1 | The adapter itself | src/lib/adapters/<platform>.ts implementing TrackerAdapter |
| 2 | Register it | adapters record in src/lib/adapters/index.ts |
| 3 | Declare the platform | VALID_PLATFORM_TYPES and DEFAULT_API_PATHS in src/lib/adapters/constants.ts |
| 4 | Credential UI | a branch in src/components/AddTrackerDialog.tsx and src/components/TrackerSettingsSheet.tsx |
| 5 | Credential size cap | LARGE_TOKEN_PLATFORMS in src/lib/limits.ts, if credentials are a JSON blob |
Steps 4 and 5 are the ones people miss. If your platform authenticates with
anything other than a single API key — session cookies, a username/password
pair, a JSON blob — the "API Token" textbox is the wrong input, and
TRACKER_TOKEN_MAX (500 characters) will reject a real cookie header at the
API boundary before the adapter ever runs. Add the platform to
LARGE_TOKEN_PLATFORMS and it gets the 5000-character cap in all three
routes at once.
Use the shared fetch helper¶
Route requests through adapterFetch (src/lib/adapters/adapter-fetch.ts)
rather than calling fetch directly. It supplies the shared timeout, honours
a user's configured proxy, and normalizes errors. It supports POST with a
body, so JSON-RPC style APIs work too.
adapterFetch parses the response as JSON. If your tracker has no API and you
have to scrape an authenticated page instead, use fetchTrackerHtml
(src/lib/adapters/html-fetch.ts) — it returns the raw HTML and handles the
browser headers, the proxy, the timeout, and sanitized errors. It takes the
parts that vary per tracker as options:
label— the tracker name used in error messagessessionExpiredMessage— thrown when the server bounces you to loginuserAgent— send the user's copied browser UA; omit it if the session came from your own login request rather than a browserfollowRedirects— set it when a redirect can be routine rather than a session expiry, and give it the pattern that identifies the login page
Only write your own fetch if neither helper fits. If you do, you must still
honour options.proxyAgent yourself, or fail loudly when one is set. Silently
ignoring it sends a user's tracker traffic straight out of their real IP when
they configured a tunnel to prevent exactly that.
Parse credential blobs with the shared helpers¶
Platforms that need more than one secret take a JSON blob in the apiToken
field. Use parseCredentialJson (src/lib/adapters/cookie-credentials.ts) to
read it — it validates that every field you name is a present, non-empty string
and produces the error wording users already see on the other adapters.
If one of those fields is a pasted Cookie header, run it through
validateCookieHeader as well. It strips a leading Cookie: prefix, and catches the
two mistakes that actually reach us: pasting a lone cookie name instead of the
header value, and a value the browser truncated mid-copy into a non-ASCII
ellipsis.
Report unknown values as null, never 0¶
If the platform's API doesn't return a field, set it to null. A hardcoded
0 renders identically to a measured 0, so the UI shows a confident wrong
number instead of "unknown". seedingCount, leechingCount, seedbonus,
requiredRatio, warned, hitAndRuns and freeleechTokens are all
nullable for this reason.
9. Common Mistakes¶
Forgetting to add to the barrel file¶
The most common mistake. If you create src/data/trackers/mytracker.ts but skip editing index.ts, the tracker won't appear in the UI. Export and import it in index.ts, and add the variable to ALL_TRACKERS.
Wrong platform type¶
Using "unit3d" for a Gazelle tracker (or vice versa) causes the adapter to send the wrong API request. The scheduler will log a fetch error or return garbled data. Check the tracker's tech stack — most UNIT3D sites document /api/user, and most Gazelle sites use /ajax.php.
Wrong apiPath¶
Each platform has a default API path (see the field reference table above). Setting apiPath: "/api/user" on a Gazelle tracker will cause every poll to fail with 404. Match what the platform actually serves.
draft: true left in a finished entry¶
If draft: true, the entry is filtered out at runtime and never shown to users. Remove the field or set draft: false when you're done.
Invalid content category names¶
The contentCategories array only accepts the fixed allowed list. A typo like "Movie" instead of "Movies" will fail validation. The list is case-sensitive.
color not a valid hex code¶
The color field must be a full six-digit hex string starting with #. Shorthand hex (#fff) and named colors don't work. Use real hex.
Logo path points to a missing file¶
If you set logo: "/tracker-logos/mytracker.svg" but the file doesn't exist in public/, the image will 404 and show broken in the UI. Add the file or set logo: "".