Go
A net/http handler using only the standard library. The same function works in chi and gin.
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>net/http
// main.go
package main
import (
"bytes"
"encoding/json"
"errors"
"net"
"net/http"
"os"
"time"
)
var nolgardClient = &http.Client{Timeout: 3 * time.Second}
type verifyResult struct {
Success bool `json:"success"`
Decision string `json:"decision"`
Error string `json:"error"`
}
func nolgardVerify(r *http.Request, token, action string, account map[string]string) (verifyResult, error) {
remoteIP, _, _ := net.SplitHostPort(r.RemoteAddr)
payload := map[string]any{
"secret": os.Getenv("NOLGARD_SECRET"),
"token": token,
"remoteIp": remoteIP,
"action": action,
}
if account != nil {
payload["account"] = account
}
body, err := json.Marshal(payload)
if err != nil {
return verifyResult{}, err
}
resp, err := nolgardClient.Post("https://nolgard.com/v1/verify", "application/json", bytes.NewReader(body))
if err != nil {
return verifyResult{}, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return verifyResult{}, errors.New(resp.Status)
}
var result verifyResult
err = json.NewDecoder(resp.Body).Decode(&result)
return result, err
}
type AccountOptions struct {
TrialDays int // 0 means your normal plan
ConfirmEmailFirst bool // hold the trial until the email is confirmed
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func signup(w http.ResponseWriter, r *http.Request) {
var data struct {
Token string `json:"token"`
Email string `json:"email"`
}
json.NewDecoder(r.Body).Decode(&data) // a bad body leaves the token empty: missing-token path
result, err := nolgardVerify(r, data.Token, "signup", map[string]string{"email": data.Email})
if err != nil {
result = verifyResult{Error: "unavailable"} // timeout or 5xx
}
if !result.Success {
switch result.Error {
case "invalid-secret", "invalid-input":
writeJSON(w, 503, map[string]string{"error": "try-again"}) // config error: fail closed
case "missing-token": // script blocked: not a bot verdict
createAccount(data.Email, AccountOptions{TrialDays: 3, ConfirmEmailFirst: true})
writeJSON(w, 200, map[string]bool{"ok": true, "confirm_email": true})
case "unavailable", "over-quota": // fail open with a reduced grant
createAccount(data.Email, AccountOptions{TrialDays: 3})
writeJSON(w, 200, map[string]bool{"ok": true})
default:
writeJSON(w, 409, map[string]string{"error": "retry-token"}) // stale token: the form retries once
}
return
}
switch result.Decision {
case "allow":
createAccount(data.Email, AccountOptions{})
case "limit":
createAccount(data.Email, AccountOptions{TrialDays: 3})
case "shadow":
// same response, nothing created
case "challenge":
writeJSON(w, 403, map[string]string{"error": "confirm-email"})
return
default:
writeJSON(w, 403, map[string]string{"error": "rejected"})
return
}
writeJSON(w, 200, map[string]bool{"ok": true})
}
func main() {
http.HandleFunc("POST /signup", signup)
http.ListenAndServe(":8080", nil)
}Needs Go 1.22 or later for the POST /signup pattern. Supply your own createAccount function; 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 same signup function works in chi with r.Post("/signup", signup) and in gin with router.POST("/signup", gin.WrapF(signup)). r.RemoteAddr is the direct peer. Behind a proxy, use the address set by your trusted proxy. Do not trust unchecked forwarding headers.