Title: PSB Bot Traffic Controller
Author: paulborile
Published: <strong>August 26, 2026</strong>
Last modified: August 26, 2026

---

Search plugins

![](https://ps.w.org/psb-bot-traffic-controller/assets/banner-772x250.png?rev=3667516)

![](https://ps.w.org/psb-bot-traffic-controller/assets/icon-256x256.png?rev=3667516)

# PSB Bot Traffic Controller

 By [paulborile](https://profiles.wordpress.org/paulborile/)

[Download](https://downloads.wordpress.org/plugin/psb-bot-traffic-controller.1.0.2.zip)

 * [Details](https://wordpress.org/plugins/psb-bot-traffic-controller/#description)
 * [Reviews](https://wordpress.org/plugins/psb-bot-traffic-controller/#reviews)
 *  [Installation](https://wordpress.org/plugins/psb-bot-traffic-controller/#installation)
 * [Development](https://wordpress.org/plugins/psb-bot-traffic-controller/#developers)

 [Support](https://wordpress.org/support/plugin/psb-bot-traffic-controller/)

## Description

PSB Bot Traffic Controller sits in front of your WordPress site and classifies incoming
traffic
 into five bot categories:

 1. AI training bots
 2. AI assistants
 3. Autonomous AI agents
 4. Search engine crawlers
 5. 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](https://github.com/matomo-org/device-detector) —
   LGPL-3.0
 * [crawler-user-agents](https://github.com/monperrus/crawler-user-agents) — MIT
 * [ai.robots.txt](https://github.com/ai-robots-txt/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

[⌊Settings page — configure block/limit/allow policy and the per-hour rate threshold
for
each of the 5 bot categories, plus a global safety-net cap applied to all bot
traffic.⌉⌊Settings page — configure block/limit/allow policy and the per-hour rate
threshold for
each of the 5 bot categories, plus a global safety-net cap applied
to all bot traffic.⌉[

Settings page — configure block/limit/allow policy and the per-hour rate threshold
for each of the 5 bot categories, plus a global safety-net cap applied to all bot
traffic.

[⌊Dashboard — mu-plugin install/health status, persistent object cache detection,
bot
pattern-data version, and a rolling per-category block/limit/allow activity 
summary.⌉⌊Dashboard — mu-plugin install/health status, persistent object cache detection,
bot
pattern-data version, and a rolling per-category block/limit/allow activity 
summary.⌉[

Dashboard — mu-plugin install/health status, persistent object cache detection, 
bot pattern-data version, and a rolling per-category block/limit/allow activity 
summary.

## Installation

 1. Upload the plugin zip via Plugins > Add New > Upload Plugin, or extract it into
    wp-content/plugins/psb-bot-traffic-controller/.
 2. 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.
 3. 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 on `wp-content/mu-
plugins/`).

### How do I reset rate-limit counters while testing?

Add `define( 'PSB_BTC_DEBUG', true );` to `wp-config.php` on 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.

Contributors

 *   [ paulborile ](https://profiles.wordpress.org/paulborile/)

[Translate “PSB Bot Traffic Controller” into your language.](https://translate.wordpress.org/projects/wp-plugins/psb-bot-traffic-controller)

### Interested in development?

[Browse the code](https://plugins.trac.wordpress.org/browser/psb-bot-traffic-controller/),
check out the [SVN repository](https://plugins.svn.wordpress.org/psb-bot-traffic-controller/),
or subscribe to the [development log](https://plugins.trac.wordpress.org/log/psb-bot-traffic-controller/)
by [RSS](https://plugins.trac.wordpress.org/log/psb-bot-traffic-controller/?limit=100&mode=stop_on_copy&format=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.InterpolatedNotPrepared` annotations
   to the
    three interpolated-table-name queries in `class-ratelimit-store-db.php`(`
   increment_and_get()`, `get_count()`, `cleanup_expired()`) — this file had none,
   unlike its sibling `class-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 existing `phpcs:ignore` comment on its
   
   TRUNCATE TABLE query: it previously named the sniff `WordPress.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 in `SUBMISSION.md` (non-atomic-counter `flock()`
   locking has no `WP_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 `producer` attribution 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 (now `psb-btc-config-cache.json`, read via `json_decode()`),
   per a review finding against writing executable code files into `wp-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 use `flock()`-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 bare `if ( ! defined( 'ABSPATH' ) ) { exit; }` review
   expects, across all affected files. Test-only standalone loading now works by
   having the PHPUnit bootstrap itself define `ABSPATH`, 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, switch `class-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 generated `bot-patterns.
   php` (via tools/cmd/build-patterns, not by hand-editing the output), and correct“
   Tested up to” to the current WordPress release. See `SUBMISSION.md` for 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 requires `define( 'BOTPOLICE_DEBUG',
   true );` in wp-config.php to become reachable at all, rather than being an unauthenticated
   endpoint present on every install. `uninstall.php` also 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 in `wp_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. Added `Botpolice_Client_Ip` (`includes/detection/
   class-client-ip.php`, documented in `CLIENT_IP.md`), which resolves the real 
   public client IP by walking the `Forwarded` (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 duplicated `botpolice_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. Added `Botpolice_Upgrade_Handler`,
   which resyncs the mu-plugin on `upgrader_process_complete` and 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_Recorder`
   and 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 plain `php -l` lint
   check without complaint. Caught by a new subprocess-based regression suite (`
   tests/php/mu-bootstrap/`), since PHPUnit cannot safely intercept `exit()`-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.

## Meta

 *  Version **1.0.2**
 *  Last updated **1 day ago**
 *  Active installations **Fewer than 10**
 *  WordPress version ** 6.0 or higher **
 *  Tested up to **7.1**
 *  PHP version ** 8.0 or higher **
 * Tags
 * [AI Crawlers](https://wordpress.org/plugins/tags/ai-crawlers/)[bots](https://wordpress.org/plugins/tags/bots/)
   [firewall](https://wordpress.org/plugins/tags/firewall/)[rate limiting](https://wordpress.org/plugins/tags/rate-limiting/)
   [security](https://wordpress.org/plugins/tags/security/)
 *  [Advanced View](https://wordpress.org/plugins/psb-bot-traffic-controller/advanced/)

## Ratings

No reviews have been submitted yet.

[Your review](https://wordpress.org/support/plugin/psb-bot-traffic-controller/reviews/#new-post)

[See all reviews](https://wordpress.org/support/plugin/psb-bot-traffic-controller/reviews/)

## Contributors

 *   [ paulborile ](https://profiles.wordpress.org/paulborile/)

## Support

Got something to say? Need help?

 [View support forum](https://wordpress.org/support/plugin/psb-bot-traffic-controller/)