Description
PSB Bot Traffic Controller sits in front of your WordPress site and classifies incoming traffic
into five bot categories:
- AI training bots
- AI assistants
- Autonomous AI agents
- Search engine crawlers
- Scraping agents
Each category can be configured independently to block (HTTP 403), rate-limit (HTTP 429 once
a configurable per-hour threshold is exceeded), or allow traffic. A global safety-net cap also
applies to all detected bot traffic regardless of per-category policy.
Detection runs via a free, zero-dependency regex/User-Agent pattern engine by default, curated
from third-party bot-pattern data (see Credits below). An optional WURFL.js-based detection
engine can be enabled for higher accuracy.
For performance, bot interception runs as early as possible via a small mu-plugin installed
automatically into wp-content/mu-plugins/ — this avoids the cost of a full WordPress
bootstrap for traffic that ends up being blocked.
Credits
The bundled bot-pattern data (includes/detection/regex/data/bot-patterns.php) is built from
three third-party sources, each under a GPL-compatible license:
- matomo-org/device-detector — LGPL-3.0
- crawler-user-agents — MIT
- ai.robots.txt — MIT
No code from these projects is bundled — only pattern/name/category data, transformed at build
time (tools/cmd/build-patterns) into this plugin’s own runtime format.
This plugin makes no outbound HTTP requests and does not integrate with any third-party or
external service. Earlier versions of the bundled pattern data included a per-signature
producer field (an attribution URL crediting the upstream source of each entry); it has been
removed entirely, since it was never fetched or otherwise dereferenced by the plugin at
runtime.
Screenshots


Installation
- Upload the plugin zip via Plugins > Add New > Upload Plugin, or extract it into wp-content/plugins/psb-bot-traffic-controller/.
- Activate the plugin. This installs a small companion file into
wp-content/mu-plugins/(WordPress’s “must-use plugin” mechanism) that performs early bot interception. - Configure per-category policy under Settings > PSB Bot Traffic Controller.
FAQ
-
Does this replace a WAF or CDN-level bot protection?
-
No — PSB Bot Traffic Controller runs inside WordPress (or, for the mu-plugin path, as early in
WordPress’s own bootstrap as possible) and cannot intercept traffic before it reaches your web
server. It complements, rather than replaces, edge-level protection. -
What happens if the mu-plugin file can’t be written on activation?
-
An admin notice is shown with the filesystem error. The plugin still activates, but early
interception won’t run until the mu-plugin is successfully installed (retry by
deactivating/reactivating, or check file permissions onwp-content/mu-plugins/). -
How do I reset rate-limit counters while testing?
-
Add
define( 'PSB_BTC_DEBUG', true );towp-config.phpon a staging/test site. This
enables a rotating-token reset URL shown on the Dashboard (valid for a few hours at a time,
then automatically expires). It is disabled by default and should never be enabled on a
production site.
Reviews
There are no reviews for this plugin.
Contributors & Developers
“PSB Bot Traffic Controller” is open source software. The following people have contributed to this plugin.
ContributorsTranslate “PSB Bot Traffic Controller” into your language.
Interested in development?
Browse the code, check out the SVN repository, or subscribe to the development log by RSS.
Changelog
1.0.2
- Re-analyzed every remaining automated Plugin Check finding against this version’s actual
code, rather than restating prior documentation, and applied two genuine fixes found in the
process (both comment-only, zero behavior change): - Added
phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPreparedannotations to the
three interpolated-table-name queries inclass-ratelimit-store-db.php
(increment_and_get(),get_count(),cleanup_expired()) — this file had none, unlike its
siblingclass-ratelimit-reset.php, despite sharing the identical unfixable-by-design
pattern (table names can never be%s/%d$wpdb->prepare()placeholders). - Corrected
class-ratelimit-reset.php‘s existingphpcs:ignorecomment on its
TRUNCATE TABLE query: it previously named the sniffWordPress.DB.PreparedSQL.NotPrepared,
which is not what actually fires there — fixed to name
WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,
matching what the checker actually reports, so the comment provides real suppression instead
of none.
1.0.1
- Regenerated the icon/banner/logo image assets from an updated source design (ring emblem +
“BOT TRAFFIC CONTROLLER” wordmark), replacing the placeholder artwork shipped in 1.0.0. - Sanitize
psb_btc_mu_respond()‘s output (strip control characters before echoing) rather
than relying on every current call site happening to pass a hardcoded literal — all 3 call
sites still do today, so this closes a latent gap rather than fixing a live exploit, but
escaping at the point of output shouldn’t depend on that staying true as the file is edited. - Re-reviewed the remaining automated Plugin Check findings against this version and confirmed
they remain the already-documented false positives / accepted architectural exceptions
covered inSUBMISSION.md(non-atomic-counterflock()locking has noWP_Filesystem
equivalent and must run pre-bootstrap;$wpdb->prepare()placeholders the checker’s static
analysis can’t trace through) — nothing else new to fix.
1.0.0
- Renamed the plugin from “botpolice” to “PSB Bot Traffic Controller” (slug
psb-bot-traffic-controller), following a WordPress.org plugin review finding that
“botpolice” too closely matched an existing project with overlapping functionality.
Every internal class/function/constant prefix, option/table/transient name, and
generated mu-plugin filename was renamed to match (Botpolice_*PSB_BTC_*,
botpolice_*psb_btc_*,BOTPOLICE_*PSB_BTC_*). - Removed the
producerattribution field from the bundled bot-pattern data
(includes/detection/regex/data/bot-patterns.php) — it held a URL in most entries and
was flagged twice by review under two headings (“calling files remotely” and
“undocumented use of a 3rd party/external service”), even though it was never
dereferenced as a URL anywhere in the runtime code. Removed entirely rather than
disclosed, since the plugin makes no outbound HTTP requests and integrates with no
external service. - Changed the mu-plugin’s per-category config cache from an executable
<?php return array(…); file to plain JSON (nowpsb-btc-config-cache.json,
read viajson_decode()), per a review finding against writing executable code
files intowp-content/mu-plugins/outside the small set of core-defined drop-in
exceptions. The mu-plugin bootstrap file itself and the (much larger) pattern-data
cache were not affected — review did not flag either of those. - Fixed a real correctness bug review’s automated tooling also caught: both the
object-cache-backed rate-limit store and the mu-plugin’s flat-file rate-limit
counters used a non-atomic read-increment-write sequence, letting concurrent
requests undercount and bypass configured limits. The object-cache store now uses
wp_cache_add()/wp_cache_incr()(atomic at the cache backend); the mu-plugin’s
flat-file counters now useflock()-based exclusive locking around the
read-modify-write. - Replaced the compound
if ( ! defined('ABSPATH') && ! defined('...STANDALONE') )
direct-access guard (used to allow PHPUnit to load classes without a full WP
install) with the literal bareif ( ! defined( 'ABSPATH' ) ) { exit; }review
expects, across all affected files. Test-only standalone loading now works by
having the PHPUnit bootstrap itself defineABSPATH, rather than each production
file special-casing a second constant.
0.4.1
- Address findings from the official WordPress Plugin Check tool: escape remaining
unescaped Dashboard output, switchclass-mu-installer.php‘s version-stamp read and
class-ratelimit-reset.php’s counter-file cleanup off raw file-handle/unlink()calls,
document the intentional nonce-free design of the (opt-in, off-by-default) debug reset
endpoint, add a direct-access guard to the generatedbot-patterns.php(via
tools/cmd/build-patterns, not by hand-editing the output), and correct “Tested up to”
to the current WordPress release. SeeSUBMISSION.mdfor a list of remaining Plugin
Check findings that are intentional design choices rather than defects (the
BOTPOLICE_STANDALONE direct-access guard variant, the mu-plugin’s necessarily-early
filesystem calls, and the reset endpoint’s token-based-not-nonce-based auth).
0.4.0
- The rate-limit-reset debug endpoint (
?botpolice_reset=<token>) is no longer loaded or
registered by default — it now requiresdefine( 'BOTPOLICE_DEBUG', true );in
wp-config.php to become reachable at all, rather than being an unauthenticated endpoint
present on every install.uninstall.phpalso now removes its secret option, if present. - Prepare for WordPress.org Plugin Directory submission: tightened the short description
to fit the directory’s display limit, corrected the “Tested up to” header (was a
nonexistent “7.0”), added a Screenshots section covering the Settings and Dashboard
admin pages, added a Credits section disclosing and licensing the bundled bot-pattern
data’s third-party sources, and fixed composer.json declaring “proprietary” while the
plugin itself is GPLv2 or later.
0.3.2
- Fix: the debug rate-limit-reset token (
?botpolice_reset=<token>, shown on the
Dashboard for manual testing) was a single fixed value stored inwp_options—
once leaked (screenshot, shared log, browser history), it stayed valid forever.
It now rotates automatically every 4 hours: the displayed/accepted token is an
HMAC-SHA256 of a persistent random secret (never displayed) and the current
4-hour time window, so a leaked token expires on its own without any manual
rotation step, and reading the plugin’s source alone is not enough to compute a
valid token (the algorithm is public; the secret it’s keyed on is not). The
Dashboard now also shows how many minutes remain before the current URL expires.
0.3.1
- Add the product logo/emblem to the Settings and Dashboard admin page headers
(assets/images/logo.png, shipped in the plugin zip) and WordPress.org-listing icon
and banner assets (.wordpress-org/, not bundled into the plugin zip per WP.org
convention — those live in the SVN assets/ directory of the plugin listing, separate
from trunk/).
0.3.0
- Fix: rate-limiting and category policy keyed on the raw TCP peer address
(REMOTE_ADDR), which is the reverse proxy’s/load balancer’s own IP on any site
running behind one (Cloudflare, nginx, AWS ALB/ELB) — collapsing every distinct
visitor onto one shared rate-limit bucket. AddedBotpolice_Client_Ip
(includes/detection/class-client-ip.php, documented inCLIENT_IP.md), which
resolves the real public client IP by walking theForwarded(RFC 7239) and
X-Forwarded-For headers before falling back to the raw peer address, and only
ever returns an internet-routable address for rate-limiting purposes. Wired into
both enforcement layers:Botpolice_Request_Context(full-WP path) and a
duplicatedbotpolice_mu_client_ip()in the self-contained mu-plugin hot path.
0.2.1
- Fix: mu-plugin bootstrap and its pattern-data cache could go stale after an upload-based
plugin upgrade (WordPress does not re-fire the activation hook on that path), silently
running old enforcement logic indefinitely. AddedBotpolice_Upgrade_Handler, which resyncs
the mu-plugin onupgrader_process_completeand via a cheap staleness check on every
admin page load, covering upgrade paths that bypass WordPress’s upgrader (e.g. manual FTP
replacement) as well.
0.2.0
- Add per-category, per-action activity recording (
Botpolice_Activity_Recorderand the
mu-plugin’s own inline counter), surfaced on the dashboard’s Recent Activity view. - Add a token-gated debug endpoint (
?botpolice_reset=<token>) to clear rate-limit counters
for iterative live-site testing without waiting out hourly buckets. - Add a curl-based live end-to-end test suite (
tests/live/test-live-site.sh,
make test-live) verifying block/limit/allow enforcement against a real deployed site.
0.1.1
- Fix: mu-plugin helper functions were declared conditionally (inside
function_exists()
guards) but called from a closure defined before those declarations executed — PHP only
hoists unconditional top-level function declarations, so any request matching a bot pattern
hit a fatal “call to undefined function” error, blocking all traffic on the plugin’s
highest-risk hot path. Passed a plainphp -llint check without complaint. Caught by a new
subprocess-based regression suite (tests/php/mu-bootstrap/), since PHPUnit cannot safely
interceptexit()-calling code in-process.
0.1.0
- Initial development release: regex/User-Agent detection engine (911 pattern entries across
5 categories, built from matomo-org/device-detector, crawler-user-agents, and ai.robots.txt),
policy engine (block/limit/allow per category plus a global rate cap), rate limiter
(transients or DB-table-backed), mu-plugin-based early interception with a
WP-API-backed fallback layer, Settings/Dashboard admin UI, and installable-zip packaging.
