How private checks work
When your store checks a customer privately, through the WooCommerce plugin or a drop-in client for Node or PHP, the customer’s details never reach us. We can’t see who you checked, and we can’t tell whether they matched. This isn’t a promise in a privacy policy. The way the lookup works makes it impossible, and you can check that yourself.
In plain words
- On your server, the plugin or client turns the customer into lookup keys: their email, phone, street, name and IP, plus near-miss versions (the email with one letter dropped, the phone with one digit dropped, street spellings, nicknames, a sound-alike of the last name).
- Each key is scrambled with a random number that only your server knows, and thrown away after the check. What we receive is a list of numbers that look random. They’d look just as random for any other customer.
- We apply our secret key to those numbers and send them back. We can’t undo your scrambling, so we still don’t know what they were.
- Your server removes its scrambling. Only a key that really belongs to a reported customer unlocks an entry in the key table your server downloads from us. Matching and scoring happen on your server.
- Your server then tells us the verdict and score, nothing else, so the order counts in the numbers on the Check page.
Check it yourself
- Read the code. The plugin is plain PHP:
includes/blind.phpin the download, with every step above inbcg_blind_check(). The drop-in clients are single files built from the same code, with the steps inPrivateCheck.check(). - Look at what leaves your store. In the plugin, WooCommerce → BadCustomers → Privacy → What was sent shows the last request of each kind, byte for byte. The clients take an
onRequest(Node) oron_request(PHP) option that hands you every request before it goes out. - Our key is pinned. The plugin and the clients check every answer against our public key and refuse any other key. We can’t quietly use a different key for your store or invent answers. This is the key:
Fingerprint (SHA-256 of the key): 1f98e69999e2b9a54e1be7374db2e4fb27d66a819c7c79c6bf8f6b861b8dfcff
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA044J0YgMN7MalDJBopGl VzkrL4MJy5qmAsMCY93y/wVyIFK2loDZL0hE5XtWJFRIQNpSEs1BUejVfWsSnvYG ZgaEBntgv6ISSDvEvwnJL2GnIDyF1O82tSCVoglbSTVblfw6pO8pT0TVUymi1gh9 EAMz+Pl0Ku3/ATnWJWAZ9sqxXlMi3ChGIekbJ3QpgrnraEk9PQTr2QDeIhgRfUu3 ymhpbur0wgtm3Lru1WzP1AuTDy0eTTZV9b6n8aNK8iOojQLqn9zMJUbhDR0QfwbX EzF1Y4X2l17MecRn8rOZ65GCpewUk0VnG3y76GnbWo2g4xYyICxuiI/heU09rWfs lQIDAQAB -----END PUBLIC KEY-----
This server’s plugin download, badcustomers-guard.zip: SHA-256 06591db7d192a43e296bb8b3594100b4f16999b2e82e34b3dfd7e373cc1efdeb
What we do see
- Which store checked, when, the order number, and how many lookup keys were sent (a customer with more details sends more).
- The verdict and score your plugin or client reports afterwards.
- When you open a matching report from a check, which report it was, since the link proves you hold a key that matches it. That’s the moment we learn of the match, because you asked to see it.
Limits, honestly
- Updates. The plugin updates itself from this site. A future version could in theory change what it sends. Each release is in the changelog. You can turn off auto-updates and read the code before you install one. The drop-in clients never update themselves: you replace the file when you choose to.
- Other ways in. Checks typed into this site, sent to
/api/v1/checkor coming from the Shopify webhook send the details to us. We don’t keep them, but those aren’t private checks. On Shopify, a custom app can use a drop-in client instead of the webhook. - Reports. Filing a report sends that customer’s details, because they are the report.
- Near misses. They’re found through shared variants rather than by comparing the two spellings. “One character off” occasionally catches a two-off pair, and “a few doors down” means within about 20–29 house numbers.
Not on WooCommerce? Drop-in clients
One file does the whole private check from your own server with your API key: the lookup keys, the blinding, the key table, the scoring and the verdict. No dependencies, our key is pinned, and the answer has the same shape as POST /api/v1/check. The customer’s details never leave your server.
- Node 18+, ESM, built-ins only: badcustomers-private.mjs
SHA-256 a8cc0b0fe5158bdbf9bf5efa5ae00c0128cc83c858df5dfbf3271eea2717aae8 - PHP 7.4+, openssl plus gmp or bcmath: badcustomers-private.php
SHA-256 bc0982382a564a6c3ee54e8be1d963688d278f7706d226dbae41ca466d858789
// Node
import { PrivateCheck } from './badcustomers-private.mjs';
const bc = new PrivateCheck({ apiKey: process.env.BC_API_KEY, cacheFile: '/var/cache/badcustomers-table.json' });
const result = await bc.check(customer, shipping, { orderRef: '1042' });
if (result.recommendation === 'hold') holdOrder();
// PHP
require 'badcustomers-private.php';
$bc = new BadCustomers\PrivateCheck( getenv( 'BC_API_KEY' ), array( 'cache_file' => '/var/cache/badcustomers-table.json' ) );
$result = $bc->check( $customer, $shipping, array( 'order_ref' => '1042' ) );
customer and shipping take the same fields as /api/v1/check. The options and errors are explained at the top of each file. Both are built from the plugin’s and the registry’s own code and tested against them, so they reach the same verdicts key for key. They don’t update themselves: when the private-check rules change, the client says so and you download it again.
The method, for developers
A blind-RSA oblivious PRF (RSA-2048, e = 65537), private-check rules version 1. For each key x (“kind|value”, for example e|bob@gmail.com):
H(x) = SHA-512("bcb1|h1|0|"+x) ‖ … ‖ SHA-512("bcb1|h1|4|"+x) as a number, mod N
store: r uniform in [2, N), send B = H(x) · r^e mod N POST /api/v1/blind/eval
us: S = B^d mod N = H(x)^d · r
store: s = S · r^-1 mod N, check s^e = H(x)
o = SHA-256("bcb1|out|" + x + "|" + s as 256 bytes)
tag = SHA-256("bcb1|tag|" + o)[0..8]
look tag up in GET /api/v1/blind/table (20-byte entries sorted by tag)
entry[8..20] XOR SHA-256("bcb1|pad|" + o)[0..12] =
report id u32 · flags u8 · category u8 · severity u8 · days since 2020-01-01 u16 · vendor u24
store: POST /api/v1/blind/verdict { check_id, risk, score }
The keys and the scoring are in the plugin’s includes/blind.php. The registry’s own copy, used to build the table, is written in JavaScript and checked against the PHP one key for key. The drop-in clients package both.