funnypot-mainnet-client¶
funnypot-mainnet-client is the PHP SDK for the funnypot mainnet IP-reputation service: check
whether an IP has a known-bad reputation, and report abuse you observe, over the mainnet /v1/*
API. It's framework-free with no runtime Composer dependencies, so it drops into any PHP 7.3+
codebase — a plain app, a framework adapter, or your own honeypot.
Reach for it when you want reputation checks or abuse reporting without pulling in the full detection engine — for example, gating requests by IP reputation in an app that already has its own attack detection, or just contributing sightings back to the network. If you want detection and reporting together, the batteries-included funnypot embedder wraps this client for you.
Opt-in and fail-open, by design
A fresh install does nothing: checking needs check_enabled and a key, reporting needs a
key. Every network call degrades to a safe unknown verdict on any fault — timeout, HTTP error,
malformed response — the SDK never throws and never blocks a request because the service is down.
Install¶
Runs on PHP 7.3 – 8.5 with no runtime dependencies. A few extensions are used when present but none are required:
| Extension / package | Used for | Required? |
|---|---|---|
ext-curl |
Preferred HTTP transport | No — falls back to a PHP stream-context transport |
ext-pdo_sqlite |
The bundled PdoSqliteReportQueue (report path only) |
No — only if you use the bundled queue |
psr/simple-cache |
Backing the verdict cache with any PSR-16 implementation via Psr16Cache |
No — only if you inject one |
Usage¶
Configure¶
Build a Config with fromArray() — PHP 7.3 has no named arguments, so an associative array keeps
the call order-independent and lets unset keys fall back to their defaults:
use Funnypot\Mainnet\Config;
$config = Config::fromArray([
'base_url' => 'https://mainnet.example', // scheme + host ONLY, no path
'key' => getenv('MAINNET_KEY'), // the sole credential
'check_enabled' => true, // opt in to reputation checks (default: off)
]);
check only runs when check_enabled is true and a key is set; report runs as soon as a
key is set, independently of check.
Check a reputation on the request path¶
Never call check() itself on a request path — it opens a socket. cachedVerdict() is the
request-path read: it consults the local verdict cache/mirror only, never touches the network, and
returns null on a miss.
use Funnypot\Mainnet\Client;
$client = new Client($config);
$result = $client->cachedVerdict($ip);
if ($result !== null && $result->isMalicious()) {
// verdict is 'malicious' or 'critical'
}
Run check($ip) from a warmer job or cron instead, so the verdict is already cached by the time a
request needs it.
Turn a verdict into allow/block/challenge¶
ReputationGate maps a CheckResult to a Decision using block_verdicts / challenge_verdicts
from your Config — the verdict itself is the recommendation; there's no server-sent action to
apply.
use Funnypot\Mainnet\ReputationGate;
$gate = new ReputationGate($client, $config);
$decision = $gate->decideCached($ip); // request path: no socket; a cache miss allows
if ($decision->isBlock()) {
$why = $decision->result(); // the CheckResult behind it, for logging
// ... deny the request
}
Report abuse¶
report() is fast, local, and key-gated — it enqueues, guarding against self-reporting, private
IPs, duplicates, and a daily cap:
report() never sends — drain() does
Enqueueing is local only; nothing reaches the network until drain() runs. Wire up delivery as
part of installing the client, not later — otherwise reports queue forever and nothing is sent.
Deliver queued reports¶
Call drain() from a cron tick, scheduler job, or worker — never from a request path, since it
opens sockets:
Or use the bundled CLI so a cron line needs no PHP of its own:
*/5 * * * * MAINNET_BASE_URL=https://mainnet.example MAINNET_KEY=... \
MAINNET_DB=/var/lib/funnypot/intel.sqlite \
/path/to/vendor/bin/funnypot-mainnet-drain >> /var/log/funnypot-drain.log 2>&1
MAINNET_SELF_IPS (comma-separated) and MAINNET_DAILY_CAP are optional environment overrides;
--limit=N caps rows per tick.
Public API¶
Everything lives under the Funnypot\Mainnet\ namespace (or a documented sub-namespace below).
Funnypot\Mainnet\Client — the entry point¶
| Method | Runs on | Notes |
|---|---|---|
new Client(Config $config, ?Transport $transport = null, ?Cache $cache = null, ...) |
— | Only $config is required; inject a Cache to enable cachedVerdict() reads. |
check(string $ip, array $opts = []) |
Out-of-band only | Opens a socket. Never throws — degrades to a fail-open CheckResult. |
cachedVerdict(string $ip, array $opts = []) |
Request path | Cache/mirror read only; null on a miss. |
report(string $ip, string $comment, string $categories = '21', array $signals = []) |
Anywhere | Enqueues; returns ['queued' => bool, 'reason' => string]. |
drain(int $limit = 200) |
Out-of-band only | Sends queued reports; returns ['sent' => int, 'failed' => int, 'pending' => int]. |
queuedReports() |
Anywhere | Rows currently waiting for delivery. |
breaker() |
Anywhere | The shared CircuitBreaker, for a host that delivers reports on its own path. |
Funnypot\Mainnet\Config — settings¶
Built only via the static Config::fromArray(array $opts). Common keys: base_url, key,
check_enabled, fail_mode (open/closed), block_verdicts, challenge_verdicts,
min_block_score, sensitivity, cache_ttl_hours, timeout_ms, self_ips, daily_cap,
dedup_hours, intel_db_path. Every key has a sane default (see the Usage example).
Funnypot\Mainnet\CheckResult — the verdict¶
Immutable result from check() / cachedVerdict(). unknown (couldn't check) is always distinct
from clean (checked, looks fine).
| Member | Description |
|---|---|
verdict() |
One of unknown, clean, suspicious, malicious, critical |
score() |
0–100, or null when unknown / fail-open |
source() |
fresh, cache, or fail-open |
isMalicious() / isSuspicious() / isFailOpen() |
Convenience predicates |
evidence() / context() / expiresAt() / scoredAs() |
Supporting detail from the service |
Funnypot\Mainnet\ReputationGate — verdict to decision¶
| Method | Runs on | Notes |
|---|---|---|
new ReputationGate(Client $client, Config $config) |
— | |
decide(string $ip) |
Out-of-band only | Runs check() then maps to a Decision. |
decideCached(string $ip) |
Request path | Maps cachedVerdict(); a miss allows. |
Funnypot\Mainnet\Decision¶
action() (allow/block/challenge), result() (the underlying CheckResult), and
isAllow() / isBlock() / isChallenge().
Funnypot\Mainnet\CircuitBreaker¶
Shared fail-open cooldown behind check() and drain(). Most consumers never touch it directly —
Client::breaker() exposes the same instance so a host with its own delivery path (rather than
calling drain()) can record outages on it via recordTransportFailure() / recordQuota(), keeping
check() and delivery in sync.
Cache adapters (Funnypot\Mainnet\Cache\)¶
Implement the Cache interface (get(), set(), has()) to plug in your own store, or use one of
the bundled ones:
| Class | Behaviour |
|---|---|
NullCache |
Default when no cache is injected — every check is fresh, nothing is stored. |
ArrayCache |
In-process only; doesn't survive past the current request. |
Psr16Cache |
Wraps any Psr\SimpleCache\CacheInterface (WordPress object cache, Laravel Cache, etc.) — the practical choice for cross-request caching. |
Custom report storage (Funnypot\Mainnet\Report\)¶
| Class | Purpose |
|---|---|
ReportQueue (interface) |
Implement this to back the report queue with your own storage (e.g. an ORM) instead of SQLite. |
PdoSqliteReportQueue |
The bundled default — new PdoSqliteReportQueue(string $path, int $queueCap = 10000). Size-capped; oldest rows drop first. |
Reporter::categoriesForProtocol(string $protocol) |
Static helper returning AbuseIPDB-style category ids for a protocol (ssh, telnet, ftp, etc.) — handy when building the $categories argument to report(). |
Funnypot\Mainnet\Version¶
Version::VERSION — the installed client version, for consumers that want to pin behaviour to it.