Rust
An axum handler using reqwest and serde_json.
Browser
<script src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" defer></script>
<script>
document.querySelector('#signup').addEventListener('submit', async (event) => {
event.preventDefault();
let token; // stays undefined when the script is blocked: the server takes the missing-token path
try {
await Nolgard.ready;
token = await Nolgard.execute({ action: 'signup' });
} catch {}
const form = event.target;
const response = await fetch('/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, email: form.elements.email.value })
});
// Show your own success or error message here.
});
</script>axum
// src/main.rs
use std::{net::SocketAddr, sync::LazyLock, time::Duration};
use axum::{Json, Router, extract::ConnectInfo, http::StatusCode, routing::post};
use serde::Deserialize;
use serde_json::{Value, json};
static NOLGARD: LazyLock<reqwest::Client> = LazyLock::new(|| {
reqwest::Client::builder()
.timeout(Duration::from_secs(3))
.build()
.expect("http client")
});
// None means timeout, network error, 5xx or an unreadable body.
async fn nolgard_verify(
token: &str,
remote_ip: &str,
action: &str,
account: Option<Value>,
) -> Option<Value> {
let mut payload = json!({
"secret": std::env::var("NOLGARD_SECRET").unwrap_or_default(),
"token": token,
"remoteIp": remote_ip,
"action": action,
});
if let Some(account) = account {
payload["account"] = account;
}
let response = NOLGARD
.post("https://nolgard.com/v1/verify")
.json(&payload)
.send()
.await
.ok()?;
if response.status().is_server_error() {
return None;
}
response.json().await.ok()
}
#[derive(Default)]
struct AccountOptions {
trial_days: u32, // 0 means your normal plan
confirm_email_first: bool, // hold the trial until the email is confirmed
}
#[derive(Deserialize)]
struct SignupForm {
#[serde(default)]
token: String,
#[serde(default)]
email: String,
}
async fn signup(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Json(form): Json<SignupForm>,
) -> (StatusCode, Json<Value>) {
let email = &form.email;
let result = nolgard_verify(
&form.token,
&addr.ip().to_string(),
"signup",
Some(json!({ "email": email })),
)
.await
.unwrap_or_else(|| json!({ "success": false, "error": "unavailable" })); // timeout or 5xx
if result["success"] != true {
return match result["error"].as_str().unwrap_or_default() {
"invalid-secret" | "invalid-input" => {
(
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({ "error": "try-again" })),
) // config error: fail closed
}
"missing-token" => {
// script blocked: not a bot verdict
create_account(
email,
AccountOptions {
trial_days: 3,
confirm_email_first: true,
},
);
(
StatusCode::OK,
Json(json!({ "ok": true, "confirm_email": true })),
)
}
"unavailable" | "over-quota" => {
// fail open with a reduced grant
create_account(
email,
AccountOptions {
trial_days: 3,
..Default::default()
},
);
(StatusCode::OK, Json(json!({ "ok": true })))
}
_ => (
StatusCode::CONFLICT,
Json(json!({ "error": "retry-token" })),
), // stale token: the form retries once
};
}
match result["decision"].as_str().unwrap_or_default() {
"allow" => create_account(email, AccountOptions::default()),
"limit" => create_account(
email,
AccountOptions {
trial_days: 3,
..Default::default()
},
),
"shadow" => {} // same response, nothing created
"challenge" => {
return (
StatusCode::FORBIDDEN,
Json(json!({ "error": "confirm-email" })),
);
}
_ => return (StatusCode::FORBIDDEN, Json(json!({ "error": "rejected" }))),
}
(StatusCode::OK, Json(json!({ "ok": true })))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/signup", post(signup));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
}Dependencies: axum 0.8, tokio with the full feature, reqwest 0.13 with the json feature, serde with derive, and serde_json. Needs Rust 1.80 or later for LazyLock. Supply your own create_account function; confirm_email_first holds the trial until the user clicks your confirmation link. Without a token (ad blocker, Brave Shields, no JavaScript) the user goes through email confirmation instead of a rejection. ConnectInfo is the direct peer. Behind a proxy, use the address set by your trusted proxy. Do not trust unchecked forwarding headers.