Skip to main content

Eligibility: Whitelist & Blacklist

The pool-gate is the standalone eligibility service behind every disbursement: it holds a per-deployment whitelist of wallet public keys (W) allowed to participate in the pool, and a blacklist of W values excluded from it. Before including a recipient in a proof, the disbursement agent asks the pool-gate for signed eligibility material (a whitelist signature and a blacklist non-membership proof) and feeds both to the prover as private inputs. A recipient who isn't whitelisted, or who is blacklisted, causes proof generation to fail closed; there is no on-chain fallback and no partial proof.

Management of the two lists is not symmetric. Whitelisting has a caller-facing path through the platform; blacklisting today is a pool-gate operator action with no platform-exposed write API. The sections below reflect that.

Identity, not address

Every endpoint on this page keys off the wallet public key W (the secp256k1 affine x‖y, 128 lowercase hex characters, no 0x prefix), not the wallet's on-chain address. W can't be derived from an address; it's only known once the wallet has signed something the platform can recover it from (a SIWX login). See Core concepts for why this matters.

Every call on this page needs a credential; see Authentication & API Access for the SIWX session, OAuth scopes, and admin-key fallback referenced below.


Whitelist​

The normal path: automatic at first login​

A recipient does not need to be whitelisted explicitly by an integrator in the common case, and nothing a Funding Party does can whitelist one: eligibility comes from W, the recipient's wallet public key, and W is not derivable from an address. It is recovered from the recipient's own SIWX login signature and pushed to the pool-gate the first time they sign in. That single login is the whole normal path.

This is also why a Funding Party never declares its recipients anywhere. Declaring an address would grant nothing — eligibility is the recipient's own signature to produce — so /api/disburse simply resolves each address against the pool subjects at the moment it pays them.

Any address that reaches a roster (POST /api/employers/add-employee) also defers eligibility to that same first login, so a rostered address that has not signed in is not payable either.

First login never happens for a recipient in delegated custody

The deferred path above assumes the recipient will eventually sign in. A recipient whose wallet is held by an MPC custodian signs nothing at onboarding, and their W cannot be recovered from an address — so waiting is waiting for an event that will not occur. The derivation that would give them an encryption key P cannot work either, since MPC signatures aren't reproducible, which leaves them permanently unpayable rather than merely late.

For those recipients you attest W and register P yourself, from the custodian's own record: Delegated Custody with DFNS → Employees in DFNS custody.

Attesting W directly, without waiting for a login​

disburse-agent attest takes a roster of (walletAddress, walletPub) pairs — plus an optional encryptionPublicKey per row — attaches each W, and pushes it to the pool-gate in the same run. It's the path for custodied recipients above, and equally for any integration that already knows its recipients' W and would rather not wait.

It carries its own credential, minted for the roster.attest capability and deliberately not the disburse key: running payroll and deciding who may be paid are different powers. See Authentication & API Access for minting it, and disburse-agent attest --help for the file format, the --dry-run check, and its exit codes.

Repairing or bulk-draining eligibility​

If a recipient's first login raced a pool-gate outage, or you're closing a gap across many recipients at once, drive the repair endpoint instead of waiting for each one to log in again:

POST /api/employers/eligibility/resync

Auth: SIWX session, restricted to the onboarding-allowlisted Funding Party wallet (same gate as /onboard).

FieldTypeRequiredDescription
afterIdintegerNoCursor: process only rows with id greater than this. Omit (or 0) to start from the beginning.
forcebooleanNoRe-drive every row holding a real W, ignoring the confirmation stamp. Only meaningful when driven with afterId, since its filter never shrinks on its own.
curl -X POST https://<platform-host>/api/employers/eligibility/resync \
-H "Cookie: sessionId=<your-session-cookie>" \
-H "Content-Type: application/json" \
-d '{}'

The call is paged: one request repairs at most one batch and returns a cursor.

{
"success": true,
"pending": 3140,
"result": "ok",
"whitelisted": 2980,
"already": 160,
"stamped": 3140,
"nextAfterId": 3140,
"hasMore": true
}

Keep calling with afterId set to the previous nextAfterId until hasMore is false. result: "disabled" means this deployment has no pool-gate configured at all (nothing to repair against); a non-"ok" result on a false success means the pool-gate rejected or failed the batch and the rows remain unrepaired for the next call.

What happens under the hood​

The resync call (and first login) ultimately write through to the pool-gate's own admin API, called server-to-server by the platform. It's not something an integrator calls directly, but it's documented here for completeness and for anyone operating a pool-gate instance:

POST {poolGateUrl}/v1/admin/whitelist         # single
POST {poolGateUrl}/v1/admin/whitelist/batch # batch
// single
{ "walletPubKey": "a1b2c3...f0a1b2" }

// batch
{ "walletPubKeys": ["a1b2c3...f0a1b2", "c9d8e7...e3f2a1"] }

Auth is the pool-gate OAuth admin:whitelist:write scope, or an X-API-Key fallback when OAuth isn't configured. 201/200 means added; 409 means already present, both treated as success (idempotent). There is no whitelist remove endpoint.

Reading whitelist status​

The disbursement agent reads a recipient's whitelist signature directly from the pool-gate before including them in a proof:

GET {poolGateUrl}/v1/whitelist/<W>
{ "signature": "<192-hex EdDSA signature>" }

404 means W is not (yet) whitelisted. Auth is a pool:read-scope bearer token, either minted by the agent itself via client-credentials or vended short-lived by the platform's /agent-edge/bootstrap so the agent never has to hold the raw OAuth client secret.

Auditing a deployment's whitelist

scripts/utils/scrape-whitelisted.sh in the ligeroclear repo walks every recipient row with a real W and checks it against the pool-gate, reporting which are whitelisted, missing, or unexpected, and can whitelist any gaps it finds with --whitelist-missing. It's an operational/debugging tool run against a specific deployment (it needs RDS and pool-gate credentials pulled from that deployment's running environment), not part of a customer integration.


Blacklist​

There is currently no platform- or agent-exposed write path for blacklisting a wallet. Blacklist entries are a pool-gate operator action, enforced entirely in-circuit: the prover proves non-membership in the blacklist as part of every disbursement proof, but neither the platform nor the disbursement agent offers an integrator-facing way to add, remove, or list blacklisted keys.

Reading blacklist non-membership​

This is what the agent itself queries, per recipient, before proving:

GET {poolGateUrl}/v1/blacklist/intervals/<prefix>

where prefix is the first 8 hex characters of H(W) (the Poseidon hash of W's four 128-bit limbs, the same hash the circuit uses). The response is a signed adjacency interval:

{
"adjacencySignatures": [
{ "left": "...", "right": "...", "timestamp": 0, "signature": "..." }
]
}

W is proven absent from the blacklist by showing its hash falls strictly inside a signed gap between two adjacent registered entries. Auth is the same pool:read bearer used for whitelist reads.

Dev/test only: managing blacklist entries directly

scripts/utils/blacklist.sh in the ligeroclear repo exercises POST {poolGateUrl}/v1/admin/blacklist (add) against a locally docker-composed pool-gate, and falls back to raw SQL against the pool-gate's own Postgres for remove, status, and list; there is no HTTP endpoint for any of those three. This script is a local test harness for simulating "this recipient is sanctioned" during development. It is not a production integration path; if your deployment needs a wallet blacklisted, that's a request to whoever operates your pool-gate instance.


Verifying signatures independently​

GET {poolGateUrl}/v1/public-key needs no auth: it returns the pool-gate's own signing key ({ "components": { "x": "...", "y": "..." } }) so a caller can verify whitelist and blacklist signatures without trusting the agent's own checks.

For a full breakdown of every credential referenced on this page, see Authentication & API Access.