# Torrent / BitTorrent DHT corpus search & crawl statistics (`apricot_inference/fast-tracker`) Actor

Search a live BitTorrent DHT metadata corpus and see how fast it is growing. Two endpoints: /search with category filters and relevance ranking, and /stats with a per-minute capture rate you can verify. No endpoint or key needed — the corpus is crawled and hosted by this Actor's operator.

- **URL**: https://apify.com/apricot\_inference/fast-tracker.md
- **Developed by:** [Raul Sota](https://apify.com/apricot_inference) (community)
- **Categories:** Other, Integrations, Videos
- **Stats:** 3 total users, 3 monthly users, 96.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## fast-tracker — BitTorrent DHT corpus search & live crawl statistics

Search a BitTorrent DHT metadata corpus that is being collected right now, and
see for yourself how fast it is growing.

You need nothing but a query. No endpoint, no key, no infrastructure — the
corpus is crawled and hosted by this Actor's operator.

| | |
|---|---|
| **`/search`** | full-text search across the corpus, with category filters and four orderings |
| **`/stats`** | how fast the corpus is actually growing, measured so you can check it |

***

### Two ways to use it

**Run it.** Pick an action, fill in a query, get an Apify dataset. Export to
JSON, CSV or Excel like any other Actor.

**Call it.** The Actor also runs as an HTTP service, so you can query it from
your own code with your Apify token:

```bash
curl -H "Authorization: Bearer $APIFY_TOKEN" \
  "https://OPERATOR--fast-tracker.apify.actor/search?q=ubuntu&category=software&sort=relevance&limit=10"

curl -H "Authorization: Bearer $APIFY_TOKEN" \
  "https://OPERATOR--fast-tracker.apify.actor/stats?window_minutes=30"
```

Both hit the same corpus and return the same records.

***

### `/search`

| Parameter | Values |
|---|---|
| `q` | free text, ≤200 chars. Separators are normalised, so `ubuntu 24 04` matches `ubuntu-24.04-desktop-amd64.iso` |
| `category` | `video` `audio` `software` `archive` `document` `image` — see the note below |
| `sort` | `seeders` (default) `newest` `relevance` `size` |
| `year` `resolution` | e.g. `2024`, `1080p` |
| `min_seeders` | integer; falls back to the DHT peer count where the tracker scrape has not run |
| `limit` `offset` | 1–200 (default 50), 0–100000 |

Each record carries `infohash`, `name`, `size_bytes`, `files`, `category`,
`seeders`, `leechers`, `dht_peers`, `first_seen`, `magnet`, and — where the
parser has reached them — `year`, `resolution`, `codec`, `season`, `episode`.

#### Categories describe file content, not intent

Worth reading before you use the filter, because it is exact and a wrong guess
returns nothing rather than less:

| category | what lands there |
|---|---|
| `archive` | `.iso` `.zip` `.rar` `.7z` `.tar` `.gz` — **including Linux and game disc images** |
| `software` | `.exe` `.msi` `.dmg` `.apk` `.deb` `.rpm` `.bin` — installers and packages |
| `video` | `.mp4` `.mkv` `.avi` `.mov` … |
| `audio` | `.mp3` `.flac` `.wav` … |
| `document` | `.pdf` `.epub` `.mobi` `.doc` … |
| `image` | `.jpg` `.png` `.gif` … |

A record is filed by whichever category holds the most *bytes*, so a release
with a large video file and a small readme is `video`.

The one that catches people: **searching `ubuntu` under `software` returns
nothing**, because the ISO is an archive. Leave the category unset unless you
know which side of that line your target falls on.

#### This is a crawl, not a catalogue

Worth setting expectations before your first query. The corpus is built by
sampling the DHT and fetching metadata for what it finds, so its contents are
whatever peers happened to be sharing when the crawler looked. It is not
seeded from any site's index and it is not curated.

Practically: broad terms work well, a specific title may genuinely be absent,
and **an empty result is a real answer rather than an error.** Run the *Crawl
statistics* action to see how large the corpus is and what categories it
actually holds.

#### About the orderings

`seeders`, `newest` and `size` are the corpus server's own orderings. The
request is forwarded and its response is returned untouched.

**`relevance` is different, and worth understanding before you rely on it.**
The underlying API has no relevance ranking: its full-text index is never asked
to score matches, and its fast index ranks by seeder count fixed at build time.
The three orderings it actually implements are seeders, size and capture time.

So relevance is computed by this Actor. It pulls a candidate window in the
API's own best-first order and re-ranks it on a transparent score — how much of
your query the name contains (0.60), whether those words appear together and in
order (0.20), how early the match starts (0.10), how much of the name is the
match so packs sink (0.05), and seeders as a tiebreak only (0.05).

That means **relevance re-ranks candidates, not the whole corpus.** A record
this score would love, sitting outside the candidate window, will not appear.
The `X-Relevance-Candidates` and `X-Relevance-Exhaustive` response headers tell
you which situation you are in. Ranking changes the order of records and
nothing else — no injected score field, no annotation.

***

### `/stats` — meant to be checked, not believed

Any dataset can claim to be fresh. The claim is only worth something if you can
arrive at the number yourself, so nothing in this report is taken from
server-side reporting. Two independent measurements:

**1. Count the records.** Walk the search API backwards through capture time
and bucket every record into whole UTC minutes by its `first_seen` field. The
rate is the count divided by the window — and the report ships the count, the
window, the pages fetched and the records examined, so you can check the
division rather than trust it.

**2. Watch the counter move.** Read the corpus row count before the walk and
again after it. If the corpus were a static dump, or the rate a stored
constant, that delta would be zero while the histogram claimed otherwise.

The two agreeing is the actual claim. Either alone is a number in a JSON file.

Three details decide whether the figure is honest, and each is a named
behaviour rather than a footnote:

- **The minute in progress is excluded.** Counting a partial minute as a whole
  one biases every rate downwards, by a random amount depending on when the run
  started.
- **Records are deduplicated by infohash.** The corpus is being written to while
  it is read; offset paging re-serves rows when new records land ahead of the
  cursor.
- **A truncated walk shortens the window instead of extrapolating.** If the page
  budget runs out, the half-counted minute is dropped from both the count and
  the divisor, and the result says `complete: false`.

#### Sample output

```jsonc
{
  "type": "fast-tracker-crawl-report",
  "generated_at": "2026-08-07T04:15:00.412Z",
  "instance_id": "4824992f720bacee",
  "corpus": {
    "torrents": 213727,
    "searchable": 213727,
    "tracker_scraped": 201668,
    "tracker_scraped_pct": 94.4,
    "newest_capture": "2026-08-07 04:14:59",
    "newest_capture_age_seconds": 31,
    "by_category": { "video": 138603, "audio": 41205, "software": 18844 }
  },
  "capture_rate": {
    "window_start": "2026-08-07T03:15:00.000Z",
    "window_end":   "2026-08-07T04:15:00.000Z",
    "window_minutes": 60,
    "captured": 8855,
    "per_minute": 147.6,
    "per_hour": 8856,
    "per_day_projected": 212544,
    "complete": true,
    "per_minute_series": [
      { "minute": "2026-08-07T03:15Z", "captured": 149 },
      { "minute": "2026-08-07T03:16Z", "captured": 143 }
      // ... one entry per minute in the window, zeroes included
    ],
    "method": {
      "derivation": "every returned record bucketed by first_seen into whole UTC minutes; captured / window_minutes",
      "excludes_minute_in_progress": true,
      "deduplicated_by": "infohash"
    },
    "accounting": {
      "pages_fetched": 45, "page_size": 200, "records_examined": 8912,
      "records_counted": 8855, "duplicates_skipped": 57, "truncated": false
    }
  },
  "cross_check": {
    "torrents_before": 213727, "torrents_after": 213802,
    "delta": 75, "elapsed_seconds": 31.2,
    "per_minute": 144.2, "confidence": "good"
  }
}
```

Written to the dataset and to the key-value store under `CRAWL_STATS` and
`OUTPUT`.

> The cross-check needs a measurable interval to mean anything. Under ten
> seconds the crawler's batch-commit boundaries dominate and the figure is
> noise, which is what `confidence` reports. Set **Cross-check dwell** to 60
> seconds or more if you intend to quote it.

Over HTTP, `/stats` is computed at most once per cache interval and shared
between concurrent callers. Without that, the endpoint would be an amplifier
pointed at the machine doing the crawling.

***

### What this Actor does not expose

The corpus host is infrastructure, not part of the product, and it stays out of
everything this Actor produces.

**Never written to a dataset, the key-value store, an HTTP response or a run
log:** the corpus endpoint's URL, hostname, IP or port; its API key;
filesystem paths; software versions; crawler tuning; any peer or DHT address.

- Credentials are **not input fields.** They live in the operator's environment,
  so there is no form field a caller can set — which also means there is no way
  to point this Actor at a host of your choosing and have it fetch that instead.
- The key travels in a header, never a query string.
- Reports identify the source only by `instance_id`, a SHA-256 prefix of the
  origin. Two reports from the same corpus share an id; the id leads nowhere.
- Every string reaching a log line, error response or failure message passes a
  redactor first. Network errors quote the host they failed to reach, and that
  is the leak that actually happens.
- Corpus statistics are copied field by field from an allow-list, so a future
  server release cannot start publishing something host-shaped by accident.

The test suite asserts this end to end: after a full run, **no file the Actor
writes contains the endpoint, its host, or the key.**

***

### Limits

| | |
|---|---|
| **Paging depth** | `offset` is capped at 100000, so about 100200 records are reachable for any one query. Narrow the query rather than paging deeper. |
| **Page size** | 200 records. |
| **Relevance cost** | One relevance request becomes several upstream requests — one per 200 candidates. |
| **Statistics cost** | The rate is counted from real records, so a 60-minute window is roughly 45 requests. Over HTTP the window is capped at 60 minutes and defaults to 10. |
| **Freshness** | Search responses are cached for 30s upstream. `year` and `resolution` are only populated for records the parser has reached. |

***

### Responsible use

This Actor searches an index of public BitTorrent DHT metadata: infohashes,
names, sizes and swarm counts. It does not host, transfer or distribute any
content, and it does not resolve to any file.

A DHT crawl indexes whatever it finds, which **includes material that is
illegal to possess in most jurisdictions**. The corpus operator is responsible
for filtering what their instance collects and serves; if you build on this
output, you are responsible for what you do with it. Results are metadata
records about swarms that exist, not an endorsement of any of them, and nothing
here is a recommendation to download anything.

Background on the collection method and its obligations:
[fast-tracker](https://github.com/raul2hot/fast-tracker) and its
[NOTICE.md](https://github.com/raul2hot/fast-tracker/blob/main/NOTICE.md).

***

### Not affiliated

with any torrent site, tracker, or index. The corpus is collected directly from
the BitTorrent DHT by the operator's own crawler.

MIT licensed.

# Actor input Schema

## `query` (type: `string`):

Free text, up to 200 characters. Separators are normalised, so "ubuntu 24 04" matches "ubuntu-24.04-desktop-amd64.iso". Leave empty to browse with filters only. This is a DHT crawl rather than a curated catalogue, so a specific title may genuinely not be present — run the Crawl statistics action to see what the corpus does contain.

## `category` (type: `string`):

Restrict to one category. These describe file content, not intent: a Linux or game ISO is "archive", while "software" means installers and packages (.exe, .msi, .deb, .rpm, .apk). Leave unset unless you are sure which one you want — the filter is exact, so the wrong guess returns nothing rather than less.

## `sort` (type: `string`):

Seeders, Newest and Size are the corpus server's own orderings, forwarded untouched. Relevance has no server-side equivalent — that API sorts by seeders, size or capture time only — so it is computed by this Actor over a candidate window drawn in seeder order. That makes it a re-ranking of candidates rather than of the whole corpus, and it needs a query.

## `minSeeders` (type: `integer`):

Drop records below this seeder count. Falls back to the DHT peer count for records the tracker scrape has not reached yet.

## `year` (type: `string`):

Release year parsed from the name, for example 2024. Only matches records the parse job has processed.

## `resolution` (type: `string`):

Resolution parsed from the name, for example 1080p.

## `maxItems` (type: `integer`):

Stop after this many records. Paging is capped at offset 100000, so roughly 100200 records are reachable for any single query — narrow the query rather than paging deeper.

## `pageSize` (type: `integer`):

The API caps this at 200. Lower it only to be gentler on the corpus host.

## `action` (type: `string`):

Search returns records to the dataset. Crawl statistics measures how fast the corpus is growing and publishes the evidence.

## `statsWindowMinutes` (type: `integer`):

How far back to count captures. At roughly 150 records per minute, 60 minutes costs about 45 requests against the corpus host.

## `statsMaxPages` (type: `integer`):

Hard ceiling on requests for the measurement. If the budget runs out before the window is covered, the result reports the shorter window it actually measured rather than extrapolating.

## `crossCheckSeconds` (type: `integer`):

The row count is read before and after the measurement; the delta is an independent estimate of the same rate. Set a dwell to widen that interval — below 10 seconds the batch writer's commit boundaries dominate and the figure is noise. 0 uses however long the measurement took.

## `relevancePoolMultiplier` (type: `integer`):

Candidates to collect per requested record. 5 means a 200-record page is chosen from 1000 candidates.

## `relevanceMaxCandidates` (type: `integer`):

Hard ceiling on the candidate window, whatever the multiplier works out to. Every 200 candidates is one request against a deployment that is also crawling.

## `standbyStatsCacheSecs` (type: `integer`):

How long a computed /stats report is reused. 0 recomputes on every request.

## `standbyStatsMaxPages` (type: `integer`):

Request ceiling for one /stats measurement. Lower than the batch budget because an HTTP caller is waiting for the response.

## `includeStats` (type: `boolean`):

Store a corpus snapshot under CRAWL\_STATS in the key-value store, so a dataset can be dated against the corpus it came from. Costs one request.

## `requestTimeoutSecs` (type: `integer`):

Per-attempt timeout for a single API request.

## `maxRequestRetries` (type: `integer`):

Retries after the first attempt, for timeouts, 5xx and rate limiting. Backoff is exponential with jitter and honours Retry-After.

## Actor input object example

```json
{
  "query": "ubuntu",
  "category": "",
  "sort": "seeders",
  "minSeeders": 0,
  "maxItems": 1000,
  "pageSize": 200,
  "action": "search",
  "statsWindowMinutes": 60,
  "statsMaxPages": 200,
  "crossCheckSeconds": 0,
  "relevancePoolMultiplier": 5,
  "relevanceMaxCandidates": 2000,
  "standbyStatsCacheSecs": 60,
  "standbyStatsMaxPages": 60,
  "includeStats": true,
  "requestTimeoutSecs": 60,
  "maxRequestRetries": 4
}
```

# Actor output Schema

## `records` (type: `string`):

Every matching record, exactly as the corpus API returned it.

## `crawlStats` (type: `string`):

Corpus size and, for a statistics run, the measured capture rate.

## `runSummary` (type: `string`):

What this run fetched, and how it was ranked.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "query": "ubuntu"
};

// Run the Actor and wait for it to finish
const run = await client.actor("apricot_inference/fast-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "query": "ubuntu" }

# Run the Actor and wait for it to finish
run = client.actor("apricot_inference/fast-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "query": "ubuntu"
}' |
apify call apricot_inference/fast-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apricot_inference/fast-tracker"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Zm91fwJXBARggmbrX/builds/1opd8r0gKYaj9Up8z/openapi.json
