Feeds
Stream each feed's real-time events and download its hourly and daily Parquet exports.
Each feed comes from a Client method that returns a Feed<T>, where T is the feed's event type. A Feed owns a clone of the client, so it is cheap to create and does not borrow the Client. 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.
pub fn proxies(&self) -> Feed<ProxyEvent>
pub fn anonymizers(&self) -> Feed<AnonymizerEvent>
pub fn torrents(&self) -> Feed<TorrentEvent>
pub fn ja4t(&self) -> Feed<Ja4tEvent>
pub fn helios_http(&self) -> Feed<HeliosHttpEvent>
pub fn helios_https(&self) -> Feed<HeliosHttpsEvent>
pub fn helios_dns(&self) -> Feed<HeliosDnsEvent>
pub fn helios_adb(&self) -> Feed<HeliosAdbEvent>
| Method | Event type | Stream scope | Export scope |
|---|---|---|---|
proxies | ProxyEvent | PROXY_FIREHOSE | PROXY_FEEDS |
anonymizers | AnonymizerEvent | ANONYMIZERS_STREAM | ANONYMIZERS_FEED |
torrents | TorrentEvent | TORRENTS_STREAM | TORRENTS_FEED |
ja4t | Ja4tEvent | JA4T_STREAM | JA4T_FEED |
helios_http | HeliosHttpEvent | HONEYPOT_HTTP_STREAM | HONEYPOT_HTTP_FEED |
helios_https | HeliosHttpsEvent | HONEYPOT_HTTPS_STREAM | HONEYPOT_HTTPS_FEED |
helios_dns | HeliosDnsEvent | HONEYPOT_DNS_STREAM | HONEYPOT_DNS_FEED |
helios_adb | HeliosAdbEvent | HONEYPOT_ADB_STREAM | HONEYPOT_ADB_FEED |
What each feed carries:
proxiescarries residential, mobile, and datacenter proxy sightings.anonymizerscarries VPN and anonymizer IP ranges.torrentscarries torrents seen on the BitTorrent DHT and trackers.ja4tcarries JA4T TCP fingerprints.- The four Helios feeds carry honeypot captures: HTTP requests, TLS ClientHellos, tunnel destinations, and Android Debug Bridge commands. Despite its name,
helios_dnsdoes not carry DNS queries.
See Firehose, Feeds, and Helios for the data behind each feed.
Streams
stream
Opens the feed's real-time event stream.
pub async fn stream(&self) -> Result<EventStream<T>>
- Connection: Each call opens a new connection. The server keeps it open for up to 30 minutes and then closes it, which ends the stream with
None. - Errors: A non-success response returns an
Error::Api, and a 429 withoutretry_aftermeans the key's monthly request budget is spent or it already has too many open streams. - Timeout: The client's
timeoutcovers opening the stream but not reading it. - Reconnecting: The API has no resume cursor, so events sent between connections are missed.
Call stream again to reconnect:
loop {
let mut events = client.proxies().stream().await?;
while let Some(event) = events.next().await {
let event = event?;
println!(
"{} {}",
event.ip.unwrap_or_default(),
event.provider.unwrap_or_default()
);
}
}
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.
Every feed streams the same way. Only the event type changes:
Streams
let mut events = client.proxies().stream().await?;
while let Some(event) = events.next().await {
let event = event?;
println!(
"{:?} {:?} {:?} {:?} {:?}",
event.ip, event.provider, event.r#type, event.country_code, event.asn
);
}
Every field of a proxy, anonymizer, torrent, or JA4T event is an Option, because the API may omit any of them. Helios events require their core fields: timestamp, tunnel_id, domain, port, protocol, and meta on HTTP, HTTPS, and DNS events, and session, sequential_id, command, and hash on ADB events. An event that lacks one returns Error::Decode, which ends the stream like any other failure.
HeliosHttpsEvent::details is None when the sensor could not parse the handshake, so check it before use.
EventStream
EventStream<T> yields a feed's events from stream, or its exports from all_exports.
next returns the next item, or None once the stream has ended.
pub async fn next(&mut self) -> Option<Result<T>>
- Results: Blank keepalive lines are skipped.
- Errors: A failure, such as a dropped connection, an event that doesn't decode, or a failed page request, is returned once, and the stream then ends.
- Cleanup: Dropping the stream closes its connection or stops fetching pages.
- Traits:
EventStream<T>implements thefuturesStreamtrait withItem = Result<T>, so it works with combinators fromfuturesortokio-stream.
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 return a 400
Error::Api. - Daily exports: While a daily export is being rolled up, requests for it return a 404
Error::Apiwithretry_afterset.
Export IDs
An export ID is one of:
"latest"- a UTC day as
"YYYY-MM-DD" - a UTC hour as
"YYYY-MM-DD/H", where a zero-padded hour is also accepted
"latest" is the feed's freshest export. Export::id values can be passed as they are. Any other string returns Error::InvalidInput before a request is sent.
The Rust SDK has no constant or helper functions for export IDs, so build the string yourself.
list_exports
Returns one page of the feed's exports, newest first.
pub async fn list_exports(&self, limit: Option<u32>, cursor: Option<&str>) -> Result<ExportPage>
let page = client.proxies().list_exports(Some(24), None).await?;
for export in &page.exports {
println!("{} {} {}", export.id, export.kind, export.row_count);
}
println!("next cursor: {:?}", page.next_cursor);
- Input:
limitsets the page size, from 1 to 500, andNoneuses the API default of 100. - Paging: Pass a page's
next_cursorascursorto fetch the page after it, or callall_exportsto read every export across pages.
all_exports
Returns a stream of every export, newest first, starting at cursor when it is set.
pub fn all_exports(&self, limit: Option<u32>, cursor: Option<&str>) -> ExportStream
where
T: 'static,
pub type ExportStream = EventStream<Export>;
let mut exports = client.proxies().all_exports(None, None);
while let Some(export) = exports.next().await {
let export = export?;
println!("{} {} {}", export.id, export.kind, export.size_bytes);
}
- Paging: It is not
async: pages oflimitexports are fetched lazily as the stream is read, and dropping the stream stops fetching. - Errors: An error is returned once and ends the stream, including an
Error::Protocolwhen the API returns a cursor it already returned.
export_meta
Returns the checksum, size, row count, and Parquet schema of an export.
pub async fn export_meta(&self, id: &str) -> Result<ExportMeta>
let meta = client.proxies().export_meta("latest").await?;
println!("{} {} {} {}", meta.id, meta.rows, meta.size, meta.checksum);
for column in &meta.schema.fields {
println!("{} {}", column.name, column.r#type);
}
download_url
Returns a presigned URL for an export's Parquet file, as a reqwest::Url.
pub async fn download_url(&self, id: &str) -> Result<Url>
let url = client.proxies().download_url("latest").await?;
println!("{:?}", url.host_str());
- 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.
- Errors: A 4xx or 5xx response returns an
Error::Api. A successful response that isn't a redirect, or a redirect without a validLocation, returnsError::Protocol.
download
Opens an export's Parquet file for reading.
pub async fn download(&self, id: &str) -> Result<Download>
- Security: The API key is sent only to the Synthient API, never to the storage host that serves the file, and errors never include the presigned URL.
- Errors: A non-success response from either returns an
Error::Api. Up to 10 redirects from the storage host are followed, and more returnError::Protocol. Requests to the storage host are not retried. - Timeout: The client's
timeoutcovers opening the download but not reading it.
Download
Download holds the contents of an export's Parquet file, read in chunks as they arrive.
content_lengthreturns the size of the file in bytes, if the storage host sent it.nextreturns the next chunk, orNoneonce the file has been read. Each chunk is abytes::Bytes, which dereferences to[u8].
pub fn content_length(&self) -> Option<u64>
pub async fn next(&mut self) -> Option<Result<Bytes>>
- Traits:
Downloadimplements thefuturesStreamtrait withItem = Result<Bytes>. - Cleanup: Dropping it closes the connection.
The SDK does not verify the file. Compare its SHA-256 with Export::checksum or ExportMeta::checksum. This program, adapted from the repository's download_export example, also uses the sha2 and hex crates and Tokio's fs and io-util features:
use sha2::{Digest, Sha256};
use synthient::{Client, ExportKind};
use tokio::fs::File;
use tokio::io::{AsyncWriteExt, BufWriter};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(std::env::var("SYNTHIENT_API_KEY")?)?;
let feed = client.proxies();
let mut exports = feed.all_exports(None, None);
let mut daily = None;
while let Some(export) = exports.next().await {
let export = export?;
if export.kind == ExportKind::Daily {
daily = Some(export);
break;
}
}
let daily = daily.ok_or("no daily export found")?;
let mut download = feed.download(&daily.id).await?;
if let Some(size) = download.content_length() {
println!("downloading {} ({size} bytes)", daily.id);
}
let mut file = BufWriter::new(File::create("proxies.parquet").await?);
let mut hash = Sha256::new();
while let Some(chunk) = download.next().await {
let chunk = chunk?;
hash.update(&chunk);
file.write_all(&chunk).await?;
}
file.flush().await?;
if hex::encode(hash.finalize()) != daily.checksum {
return Err("checksum mismatch".into());
}
Ok(())
}