


gem 'invisible_captcha'
<%= form_for(@topic) do |f| %>
<%= invisible_captcha %>
<% end %>
class TopicsController < ApplicationController
invisible_captcha only: [:create, :update] #, honeypot: :subtitle
# (note about multi-process)
end


Set up Cloudflare Turnstile in this project end to end.
Plan insertion points, create the widget, embed it on the right forms,
wire canonical server-side siteverify in my existing backend,
and validate the integration.
The full Turnstile Spin skill is at
https://developers.cloudflare.com/turnstile/spin/prompt.md.
Fetch it now if you do not already have it loaded.
Domains: localhost,127.0.0.1,www.snowpool.org,snowpool.org
Insertion preference: /users/sign_up, /pools/new
(the skill file is 419 lines long!)
def post_form
form = { "secret" => our_secret, "response" => clients_token }
form["remoteip"] = remote_ip if remote_ip.present?
#this helps with detection, is optional
http.post(
SITEVERIFY_URL.path,
URI.encode_www_form(form),
"Content-Type" => "application/x-www-form-urlencoded"
)
end
# in concern
def turnstile_verified?(action)
Turnstile.verify(
token: params[Turnstile::TOKEN_PARAM],
action: action, # tokens are short lived, and validated against the site + action
remote_ip: request.remote_ip
)
end
connect() {
loadTurnstileApi()
.then((turnstile) => {
this.widgetId = turnstile.render(this.element, {
sitekey: this.sitekeyValue,
action: this.actionValue,
appearance: this.appearanceValue
})
})
.catch((error) => console.warn('[turnstile]', error.message))
}
disconnect() {
if (!this.widgetId || !window.turnstile) return
// important to have disconnect, otherwise you get stale tokens on back-button.
// Claude wired this up first time.
window.turnstile.remove(this.widgetId)
this.widgetId = undefined
}


Spam has gone to zero.
# lots of options here. Need to update the gem to get the latest list
validates :email, 'valid_email_2/email': { disposable_domain: true }
validates :email, 'valid_email_2/email': { strict_mx: true, dns_timeout: 10 }
heuristic for spammy content (e.g. high entropy names)moderation queue for new users (if spammy)rate_limit to: 10, within: 3.minutes, only: :create (or Rack::Attack)
Amazon required us to implement changes because of complaint rates. Implemented the "List-Unsubscribe-Post": "List-Unsubscribe=One-Click"