Feeds

View as markdown

Stream each feed's real-time events and download its hourly and daily Parquet exports.

Each feed is a property of SynthientClient with type Feed<TEvent>, where TEvent is the feed's event type. Every feed has a real-time stream and hourly Parquet exports that roll up into daily exports. Streaming and exports need separate scopes on the API key. The event types are listed under Event types.

public sealed class Feed<TEvent>
PropertyEvent typeStream scopeExport scope
ProxiesProxyEventPROXY_FIREHOSEPROXY_FEEDS
AnonymizersAnonymizerEventANONYMIZERS_STREAMANONYMIZERS_FEED
TorrentsTorrentEventTORRENTS_STREAMTORRENTS_FEED
Ja4tJa4tEventJA4T_STREAMJA4T_FEED
HeliosHttpHeliosHttpEventHONEYPOT_HTTP_STREAMHONEYPOT_HTTP_FEED
HeliosHttpsHeliosHttpsEventHONEYPOT_HTTPS_STREAMHONEYPOT_HTTPS_FEED
HeliosDnsHeliosDnsEventHONEYPOT_DNS_STREAMHONEYPOT_DNS_FEED
HeliosAdbHeliosAdbEventHONEYPOT_ADB_STREAMHONEYPOT_ADB_FEED

What each feed carries:

  • Proxies carries residential, mobile, and datacenter proxy sightings.
  • Anonymizers carries VPN and anonymizer IP ranges.
  • Torrents carries torrents seen on the BitTorrent DHT and trackers.
  • Ja4t carries JA4T TCP fingerprints.
  • The four Helios feeds carry honeypot captures: HTTP requests, TLS ClientHellos, tunnel destinations, and Android Debug Bridge commands. Despite its name, HeliosDns does not carry DNS queries.

See Firehose, Feeds, and Helios for the data behind each feed.

Streams

StreamAsync

Streams the feed's events in real time.

public IAsyncEnumerable<TEvent> StreamAsync(CancellationToken cancellationToken = default)
  • Connection: Each enumeration opens a new connection, which the server keeps open for up to 30 minutes and then closes, ending the enumeration without an error. Opening the connection is retried like other feed requests. Leaving the loop closes the connection.
  • Cancellation: The token can also be passed with WithCancellation, and cancelling it throws OperationCanceledException.
  • Errors: A non-200 response throws an ApiException. A 429 without RetryAfter means the key's monthly request budget is spent or it already has too many open streams. An event the SDK cannot decode throws a SynthientException.
  • Timeout: If nothing arrives for longer than SynthientClientOptions.Timeout, not even the blank keepalive lines the API sends, the enumeration throws an ApiTimeoutException. A slow consumer does not trigger the timeout.
  • Reconnecting: Events sent between connections are not replayed.

The API has no resume cursor, so enumerate again to reconnect:

while (!cancellationToken.IsCancellationRequested)
{
    await foreach (var proxy in client.Proxies.StreamAsync(cancellationToken))
    {
        Console.WriteLine($"{proxy.IP} {proxy.Provider} {proxy.Type}");
    }
}

The loop reconnects immediately after a clean close. Opening a connection already uses the SDK's bounded retries, including Retry-After handling. Any error that remains after those retries, or occurs while reading the stream, stops this consumer. A 429 without Retry-After can indicate an exhausted monthly request budget or too many open streams; resolve that condition before reconnecting. Cancelling the token throws OperationCanceledException during iteration.

Every feed streams the same way. Only the event type changes:

Streams

await foreach (var proxy in client.Proxies.StreamAsync())
{
    Console.WriteLine($"{proxy.IP} {proxy.Provider} {proxy.Type} {proxy.CountryCode} {proxy.Asn}");
}

HeliosHttpsEvent.Details is null when the sensor could not parse the handshake, so check it before use.

Exports

Each feed has hourly Parquet exports that roll up into daily exports.

  • Hourly exports: An hourly export is available until its day's daily export is rolled up, shortly after 00:30 UTC the next day. After that, requests for the hour fail with a 400 ApiException.
  • Daily exports: While a daily export is being rolled up, requests for it fail with a 404 ApiException that has RetryAfter set.

Export IDs

An export ID is one of:

  • ExportId.Latest
  • a UTC day as "YYYY-MM-DD"
  • a UTC hour as "YYYY-MM-DD/H", where a zero-padded hour is also accepted

Export.Id values can be passed as they are. On the first page of a listing, the newest hourly export has the ID latest, a moving alias for the freshest export. To keep a stable ID, build it from the export's Date and Hour with ExportId.Hourly(export.Date, export.Hour!.Value), such as 2026-10-14/21. An ID is checked when it is used, and an invalid one throws an ArgumentException before any request is sent.

  • Name
    ExportId.Latest
    Type
    const string
    Description

    "latest", the feed's freshest export: the newest hourly export during the day, or the daily export once that day's hours are rolled up.

ExportId.Daily returns the ID of the daily export for a UTC day. ExportId.Hourly returns the ID of the hourly export for an hour of a UTC day.

public static string Daily(DateOnly day)
public static string Hourly(DateOnly day, int hour)
var now = DateTime.UtcNow;
var yesterday = ExportId.Daily(DateOnly.FromDateTime(now.AddDays(-1)));
var lastHour = now.AddHours(-1);
var hourly = ExportId.Hourly(DateOnly.FromDateTime(lastHour), lastHour.Hour);
Console.WriteLine($"{yesterday} {hourly}");
  • Input: Both take a DateOnly, so convert a time to UTC before taking its date. ExportId.Hourly throws an ArgumentOutOfRangeException if hour is outside 0 to 23.

ListExportsAsync

Returns one page of the feed's exports, newest first.

public Task<ExportPage> ListExportsAsync(int? limit = null, string? cursor = null, CancellationToken cancellationToken = default)
var page = await client.Proxies.ListExportsAsync(limit: 24);
foreach (var export in page.Exports)
{
    Console.WriteLine($"{export.Id} {export.Kind} {export.RowCount}");
}
Console.WriteLine($"next cursor: {page.NextCursor}");
  • Cost: It costs no credits.
  • Input: limit is the page size, from 1 to 500, and null uses the API default of 100. cursor continues a listing from a previous page's NextCursor. A limit below 1 throws an ArgumentOutOfRangeException without a request.
  • Paging: Use ListAllExportsAsync to read every page.

ListAllExportsAsync

Returns every export of the feed, newest first, fetching pages lazily as enumeration proceeds.

public IAsyncEnumerable<Export> ListAllExportsAsync(int? limit = null, string? cursor = null, CancellationToken cancellationToken = default)
await foreach (var export in client.Proxies.ListAllExportsAsync())
{
    Console.WriteLine($"{export.Id} {export.Kind} {export.SizeBytes}");
}
  • Paging: Leaving the loop early fetches no more pages.
  • Errors: A failure on any page ends the enumeration with its exception, and a page that repeats a cursor the API already returned throws a SynthientException.
  • Input: limit and cursor work as in ListExportsAsync. An invalid limit throws when the method is called rather than when enumeration starts.

GetExportMetaAsync

Returns the checksum, size, row count, and Parquet schema of an export.

public Task<ExportMeta> GetExportMetaAsync(string id, CancellationToken cancellationToken = default)
var meta = await client.Proxies.GetExportMetaAsync(ExportId.Latest);
Console.WriteLine($"{meta.Id} {meta.Rows} {meta.Size} {meta.Checksum}");
foreach (var column in meta.Schema.Fields)
{
    Console.WriteLine($"{column.Name} {column.Type}");
}
  • Cost: It costs no credits.

GetDownloadUrlAsync

Returns a presigned URL for an export's Parquet file.

public Task<Uri> GetDownloadUrlAsync(string id, CancellationToken cancellationToken = default)
var url = await client.Proxies.GetDownloadUrlAsync(ExportId.Latest);
Console.WriteLine(url.Host);
  • Lifetime: The URL is valid for 24 hours.
  • Security: The URL grants access without an API key, so treat it as a secret.
  • Use: It is useful for tools that read Parquet over HTTPS directly.

DownloadAsync

Opens an export's Parquet file for reading.

public Task<Stream> DownloadAsync(string id, CancellationToken cancellationToken = default)
  • Cleanup: Dispose the returned stream when done.
  • Security: The API key is sent only to the Synthient API, never to the storage host that serves the file, and exceptions never include the presigned URL.
  • Errors: An error status from either throws an ApiException, and one from the storage host carries only the status code. Downloads from the storage host are not retried.
  • Cancellation: cancellationToken covers opening the file, so pass a token to each read to bound the rest.
  • Timeout: A read that stalls for longer than SynthientClientOptions.Timeout throws an ApiTimeoutException.

The SDK does not verify the file. Compare its SHA-256 with Export.Checksum or ExportMeta.Checksum. Read the checksum and download the file by the same stable ID, because ExportId.Latest can move to a newer export between the two calls:

using System.Security.Cryptography;
using Synthient;

using var client = new SynthientClient();
var yesterday = ExportId.Daily(DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)));
await SaveExportAsync(client.Proxies, yesterday, "proxies.parquet");

static async Task SaveExportAsync<TEvent>(Feed<TEvent> feed, string id, string path)
{
    var meta = await feed.GetExportMetaAsync(id);
    using var sha256 = SHA256.Create();
    await using (var body = await feed.DownloadAsync(id))
    await using (var file = new CryptoStream(File.Create(path), sha256, CryptoStreamMode.Write))
    {
        await body.CopyToAsync(file);
    }

    if (!string.Equals(Convert.ToHexString(sha256.Hash!), meta.Checksum, StringComparison.OrdinalIgnoreCase))
    {
        throw new InvalidDataException("checksum mismatch");
    }
}