Rust · Gender prediction
Batch gender predictions in Rust with serde
This example uses /v1/gender/batch to send three items in one request: two names with country hints and one email address. The Item struct uses r#type because type is a reserved word in Rust, and skip_serializing_if drops the country field when it is None. Each result carries either data with a prediction or error with a code and status, and the code matches on both Options to cover every combination.
Batches are all or nothing at validation time, so a 400 means none of the items ran. A 200 can still contain item-level errors, and those items should be retried individually later rather than by resending the whole batch. The rate-limit loop resends only when a Retry-After header is present, because a rate_limited response means the batch never ran. Unknown predictions are printed with their reason.
Steps
- 1
Paste the example into src/main.rs and export FINOWL_API_KEY.
- 2
Build items with unique ids of up to 64 characters, at most 50 per request.
- 3
Run cargo run and check that each id in the output matches an id you sent.
- 4
Persist identified and unknown results, and queue only item-level errors for a later single retry.
- 5
Show predictions in aggregate views, or as a suggestion that users can correct.
cargo add reqwest --features json
cargo add serde --features derive
cargo add serde_json thiserror
cargo add tokio --features fulluse reqwest::{header::RETRY_AFTER, Client, StatusCode};
use serde::{Deserialize, Serialize};
use std::{error::Error, time::Duration};
#[derive(Serialize)]
struct Item<'a> {
id: &'a str,
r#type: &'a str,
value: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
country: Option<&'a str>,
}
#[derive(Deserialize)]
struct Prediction {
result_status: String,
gender: Option<String>,
confidence: Option<f64>,
reason: Option<String>,
}
#[derive(Deserialize)]
struct ItemError { code: String, status: u16 }
#[derive(Deserialize)]
struct ItemResult { id: String, data: Option<Prediction>, error: Option<ItemError> }
#[derive(Deserialize)]
struct BatchResponse { data: Vec<ItemResult> }
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let key = std::env::var("FINOWL_API_KEY")?;
let client = Client::builder().timeout(Duration::from_secs(30)).build()?;
let items = [
Item { id: "c1", r#type: "name", value: "Andrea", country: Some("IT") },
Item { id: "c2", r#type: "name", value: "Jean", country: Some("fr") },
Item { id: "c3", r#type: "email", value: "li.wei@example.com", country: None },
];
let res = loop {
let res = client.post("https://api.finowl.co/v1/gender/batch")
.bearer_auth(&key).json(&serde_json::json!({ "items": items })).send().await?;
if res.status() != StatusCode::TOO_MANY_REQUESTS { break res; }
// rate_limited means the batch never ran, so resending it is safe.
let Some(secs) = res.headers().get(RETRY_AFTER).and_then(|v| v.to_str().ok()?.parse().ok()) else {
return Err("quota_exceeded: buy credits or wait for the monthly reset".into());
};
tokio::time::sleep(Duration::from_secs(secs)).await;
};
match res.status() {
StatusCode::BAD_REQUEST => return Err("batch failed validation; no items ran".into()),
StatusCode::UNAUTHORIZED => return Err("FINOWL_API_KEY is missing or invalid".into()),
s if !s.is_success() => return Err(format!("Finowl returned {s}; do not resend blindly").into()),
_ => {}
}
for item in res.json::<BatchResponse>().await?.data {
match (item.data, item.error) {
(Some(p), _) if p.result_status == "unknown" => println!("{}: unknown ({:?})", item.id, p.reason),
(Some(p), _) => println!("{}: {} {:.2}", item.id, p.gender.as_deref().unwrap_or("-"), p.confidence.unwrap_or_default()),
(None, Some(e)) => println!("{}: item error {} ({})", item.id, e.code, e.status),
(None, None) => println!("{}: empty result", item.id),
}
}
Ok(())
}IN PRODUCTION
Production tips
Use your database primary key as the item id. Results come back in request order with index and id, so you can update rows without keeping a separate mapping table in memory.
Batch items default to ai_mode off, which keeps them dataset-only. The charged_credits value on each result tells you exactly what was billed for that item.
Spread large jobs over time. Each batch counts as one of your 60 requests per minute, so 50 items per request lets a single worker process thousands of names a minute.
Each lookup costs 1 credit (3 when it requests AI), with 100 free credits every month. See pricing and error codes.
A FEW GOOD QUESTIONS
Common questions
Related guides
- Check email breaches in Rust with reqwest and serde
- Check breached passwords in Rust with reqwest
- Predict gender from names in Python
- Predict gender from a name in Node.js
- Batch gender prediction in TypeScript
- Predict gender from names in Go with net/http
- Gender analytics from names for fintech audiences
- Gender insights from names for e-commerce analytics
- Gender estimates for SaaS user research and analytics
Batch gender predictions in Rust with serde
Grab an API key, paste the example and run it. 100 free credits every month.