Skip to content

Distributions ​

Exported from distributions.ts. These back GET /v1/distributions/pending and GET /v1/distributions/pending/events — the keeper feed for discovering splits with a settlement payout currently claimable via 0xSplits' permissionless distribute(). Not a typical merchant integration surface — this is for a bot/script calling distribute() directly to earn the (small) distributorFeePercent incentive.

PendingDistributionSchema ​

One entry in the data array GET /v1/distributions/pending returns:

ts
import { PendingDistributionSchema, type PendingDistribution } from '@klappay/types'

const distribution: PendingDistribution = PendingDistributionSchema.parse({
  splitAddress: '0x727cddb8a015f76c4e8a5c566c7e911761e268fe',
  network: 'base',
  token: 'USDC',
  recipients: [
    { address: '0x6EadA5173e0ad685455cEFbe4B3df023c0A598A8', percentAllocation: 99.0991 },
    { address: '0x44Ef725cA64CDAf48fEdb2AED2b156ad32537d41', percentAllocation: 0.9009 },
  ],
  distributorFeePercent: 0.1,
  estimatedRewardAmount: 0.001,
  availableSince: '2026-07-28T23:45:52.382Z',
  graceEndsAt: '2026-07-28T23:50:52.382Z',
})

recipients is always the exact array to pass to distribute() — 0xSplits only stores a hash of the recipient config on-chain, so a caller must supply the identical array to prove it matches; there's no partial/reconstructed version. estimatedRewardAmount is an estimate from the amount Klappay detected on-chain, not a live balance read — always read the split's real balance yourself before submitting a transaction, the same way Klappay's own settlement worker does. graceEndsAt is when Klappay's own worker may claim it; calling distribute() after that point is still possible but increasingly likely to lose the race.

Each entry of recipients is PendingDistributionRecipientSchema / PendingDistributionRecipient — { address, percentAllocation }, exported separately so a caller building the distribute() argument list can type one recipient without destructuring the whole distribution. percentAllocation is a percentage, not a fraction (99.0991 means 99.0991%), and the array is reproduced verbatim rather than normalized — it has to hash to exactly what the split contract stores.

SplitDistributionStatusSchema / SplitDistributionStatus is a related but distinct enum: 'pending' | 'processing' | 'completed' | 'failed', the status of one payout attempt to the merchant for a single (token, network) pair. A charge settling across more than one pair has one per pair. pending is queued and unclaimed; processing means a distributor — Klappay's own worker, or anyone who raced to call distribute() first — has claimed it and is submitting the transaction; completed means the merchant's wallet has the funds; and failed means every automatic retry was exhausted. A distribution can also stay pending past its usual retry window when the token issuer restricts an address the payout needs (the split itself as sender, or a recipient that can't receive): Klappay holds it, rechecks hourly and pays out once the restriction is lifted, without sending a webhook. See token restrictions. It describes the payout attempt, whereas the schemas above describe the work still waiting to be claimed.

PaginatedPendingDistributionsSchema — the actual GET /v1/distributions/pending response ​

ts
import { PaginatedPendingDistributionsSchema, type PaginatedPendingDistributions } from '@klappay/types'

const page: PaginatedPendingDistributions = PaginatedPendingDistributionsSchema.parse(
  await res.json(),
)
// page.data: PendingDistribution[], page.hasMore: boolean, page.nextCursor: string | null

Cursor-paginated, same shape and semantics as every other list endpoint (e.g. GET /v1/charges) — pass ?limit=/?cursor= (PaginationQuerySchema) to page through it. There's no fixed row cap anymore: during an anomalous backlog (e.g. a stalled RPC inflating the number of still-claimable rows), keep following nextCursor instead of only ever seeing however many rows fit in one response.

PendingDistributionEventSchema ​

The shape of each Server-Sent Event's data: line from GET /v1/distributions/pending/events — a discriminated union on type:

ts
import { PendingDistributionEventSchema, type PendingDistributionEvent } from '@klappay/types'

const event: PendingDistributionEvent = PendingDistributionEventSchema.parse(
  JSON.parse(sseEventData),
)

if (event.type === 'distribution.available') {
  tryClaim(event.distribution) // PendingDistribution, fully typed
} else {
  cancelPendingAttempt(event.splitAddress) // already claimed by someone else, or by Klappay
}

distribution.claimed fires the moment Klappay's own worker claims a row too — before it has actually finished calling distribute() — not only once settlement completes, so a keeper stops racing an attempt already in flight. If that attempt then fails and is scheduled for retry, distribution.available fires again with a refreshed graceEndsAt.

By default the stream sends no initial snapshot — open it before calling GET /v1/distributions/pending, then treat both the snapshot response and every event received (before or after that call resolves) as an idempotent add/remove against one local Map<splitAddress, PendingDistribution>. Opening the stream after the snapshot call instead leaves a real, if small, gap where a delta between the two requests is never delivered.

ListenPendingDistributionsQuerySchema — a self-contained alternative ​

ts
import { ListenPendingDistributionsQuerySchema } from '@klappay/types'

// GET /v1/distributions/pending/events?limit=50
ListenPendingDistributionsQuerySchema.parse({ limit: '50' }) // { limit: 50 }

Pass ?limit= (up to PAGINATION_LIMIT_MAX, default 0) and the connection sends up to that many currently-claimable distributions as synthetic distribution.available events immediately after connecting, before live deltas continue — one connection instead of the two-request dance above, with no ordering footgun (the subscription opens before the snapshot query runs, so nothing published in between is lost). It isn't a full page, though: if more than limit are claimable at connect time, the excess simply isn't sent — reach for the paginated GET /v1/distributions/pending directly if you need a complete listing during an anomalous backlog. Omitting ?limit= (or passing 0) keeps the original two-step behavior above unchanged.

Authentication and environment scoping ​

Both endpoints require the same API key used everywhere else (Authorization: Bearer klap_live_.../klap_test_...). A test key only ever sees test distributions, live only live — there is no environment field on either schema above, since the connection is already scoped by whichever key opened it.

Public docs for @klappay/types, mirrored from this package's own docs/ folder.