Java
A Spring Boot controller using the JDK's HttpClient and Jackson. No extra HTTP library.
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>Spring Boot
// SignupController.java
package com.example.signup;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SignupController {
private static final URI VERIFY_URL = URI.create("https://nolgard.com/v1/verify");
private static final Map<String, Object> UNAVAILABLE = Map.of("success", false, "error", "unavailable");
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
private final ObjectMapper json = new ObjectMapper();
private final AccountService accounts; // your own bean: create(email, trialDays, confirmEmailFirst)
public SignupController(AccountService accounts) {
this.accounts = accounts;
}
public record SignupRequest(String token, String email) {}
@PostMapping("/signup")
public ResponseEntity<Map<String, Object>> signup(@RequestBody SignupRequest body, HttpServletRequest request) {
String email = Objects.requireNonNullElse(body.email(), "");
String token = Objects.requireNonNullElse(body.token(), "");
Map<String, Object> result = nolgardVerify(token, "signup", request.getRemoteAddr(), Map.of("email", email));
if (!Boolean.TRUE.equals(result.get("success"))) {
String error = String.valueOf(result.get("error"));
switch (error) {
case "invalid-secret", "invalid-input" -> { // config error: fail closed
return ResponseEntity.status(503).body(Map.of("error", "try-again"));
}
case "missing-token" -> { // script blocked: not a bot verdict
accounts.create(email, 3, true);
return ResponseEntity.ok(Map.of("ok", true, "confirm_email", true));
}
case "unavailable", "over-quota" -> { // fail open with a reduced grant
accounts.create(email, 3, false);
return ResponseEntity.ok(Map.of("ok", true));
}
default -> { // stale token: the form retries once
return ResponseEntity.status(409).body(Map.of("error", "retry-token"));
}
}
}
String decision = String.valueOf(result.get("decision"));
switch (decision) {
case "allow" -> accounts.create(email, null, false); // null: full trial
case "limit" -> accounts.create(email, 3, false);
case "shadow" -> { } // same response, nothing created
case "challenge" -> {
return ResponseEntity.status(403).body(Map.of("error", "confirm-email"));
}
default -> {
return ResponseEntity.status(403).body(Map.of("error", "rejected"));
}
}
return ResponseEntity.ok(Map.of("ok", true));
}
private Map<String, Object> nolgardVerify(String token, String action, String remoteIp, Map<String, Object> account) {
Map<String, Object> payload = new HashMap<>();
payload.put("secret", Objects.requireNonNull(System.getenv("NOLGARD_SECRET"), "NOLGARD_SECRET is not set"));
payload.put("token", token);
payload.put("remoteIp", remoteIp);
payload.put("action", action);
if (account != null) {
payload.put("account", account);
}
try {
HttpRequest verify = HttpRequest.newBuilder(VERIFY_URL)
.timeout(Duration.ofSeconds(3))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json.writeValueAsString(payload)))
.build();
HttpResponse<String> response = HTTP.send(verify, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 500) {
return UNAVAILABLE;
}
return json.readValue(response.body(), new TypeReference<Map<String, Object>>() {});
} catch (IOException e) { // timeout, network error or bad JSON
return UNAVAILABLE;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return UNAVAILABLE;
}
}
}Supply your own AccountService bean with create(email, trialDays, confirmEmailFirst); trialDays null means the full trial, and confirmEmailFirst 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. The code targets Spring Boot 3 with Jackson 2. If you use Spring Security, keep CSRF protection on and send its token with the form. Behind a proxy, getRemoteAddr() returns the proxy address; set server.forward-headers-strategy only when your proxy is trusted. Do not trust unchecked forwarding headers.