Skip to content

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

composer require metrictower/funnypot-mainnet-client

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:

$client->report($ip, 'Scanner probing wp-login.php', '18,21');

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:

$client->drain(); // {sent:int, failed:int, pending:int}

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() 0100, 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.