DocsReference

HTTP API

The web app exposes the data it renders as JSON. The routes are read-only, unauthenticated, and served from the same origin as the app (https://<host>/api/...). They exist so that bots, dashboards and other front ends can reuse plop's holder index and split engine instead of re-implementing them. Everything they return can also be derived from chain data alone; see Reading the data.

Conventions#

  • Amounts are decimal strings of base units ("235436800000000000000000"), never JSON numbers. Convert with the token's decimals, which every response includes.
  • Times are unix seconds from block headers (czas), never wall-clock time. Countdowns are relative to the czas of the same response.
  • Counts, block numbers and seconds are numbers.
  • Errors are { "blad": "<sentence>" } with a 4xx or 5xx status. 400 malformed input, 404 unknown dropper, 409 the instance is not configured for this (no registry on its chain, or a token mismatch), 502 the chain did not answer.
  • Responses are cached in the server's memory for 10 to 20 s and requests for the same thing are coalesced. ?swiezo=1 bypasses the cache; use it sparingly, the chain's RPC rate-limits aggressively.
  • The registry is permissionless: symbols and names in these responses are whatever a creator wrote. Identify coins by address.
  • An instance may hide coins it does not want to show (test coins, spam). Hidden droppers and tokens are absent from /api/monety and answer 404 with This coin is not shown on this site. everywhere else. The chain is unaffected.
  • Field names are Polish and mirror the contracts' vocabulary (kroplomierz = dropper, saldo = balance, doNastepnej = seconds to the next drop). Every table below gives the meaning, and the glossary has the full list.

The shapes below are the TypeScript types in web/components/dto.ts.

GET /api/monety#

The dashboard list: every registered dropper with its state and token metadata, read in one multicall pinned to one block.

QueryValuesDefault
zakladkakapie (dripping), pauza (paused), puste (empty), wszystkie (all)kapie
stronapage number, 30 per page1
szukajcase-insensitive match on dropper address, token address (full or prefix), symbol or namenone
swiezo1 to bypass the 10 s cache

A dropper is empty when its balance is smaller than one drop (saldo < reguly.kwota; or zero when kwota is 0), paused when wstrzymane, dripping otherwise. A paused, empty dropper is in both tabs.

The route reads at most 500 registry entries; beyond that klopot says so.

{
  "monety": [
    {
      "kroplomierz": "0x1234567890abcdef1234567890abcdef12345678",
      "token": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
      "wlasciciel": "0x9999999999999999999999999999999999999999",
      "logo": "ipfs://bafkreiexamplecidexamplecidexamplecidexamplecidexamplecid",
      "utworzono": 1789482286,
      "symbol": "DEMO",
      "nazwa": "Demo coin",
      "decimals": 18,
      "podaz": "1000000000000000000000000000",
      "saldo": "941747200000000000000003",
      "doNastepnej": 74,
      "epoka": 1,
      "wstrzymane": false,
      "domknieta": true,
      "maksNaEpoke": "235436800000000000000000",
      "minOdstep": 120,
      "wykonawca": "0x5e25b93C6454C10E4110B70DAFa76C6934cDee03",
      "reguly": {
        "kwota": "235436800000000000000000",
        "minSaldo": "0",
        "sufitNaAdres": "0",
        "minCzasTrzymania": 0,
        "okresBezruchu": 3600,
        "porowno": false,
        "pomijajKontrakty": true,
        "pomijajWlasne": true
      }
    }
  ],
  "ile": 1,
  "strona": 1,
  "naStronie": 30,
  "czas": 1789488585,
  "blok": 63787090,
  "fabryka": "0x1CE8f5785617eaf9B6BB3c77CE7d242D86Dd0502",
  "klopot": null
}
FieldMeaning
kroplomierzdropper address, the identity of a coin in plop and the key of /api/moneta
wlascicielowner at registration; display only
logohttps:// or ipfs:// URI or null; sanitised, render through your own proxy or an IPFS gateway
saldolive balanceOf(dropper)
doNastepnejseconds after czas at which a new epoch may open; 0 = now
epokaepochs so far, i.e. number of drops
domknietathe current epoch is closed
wykonawcaexecutor; 0x000…000 means nobody can drop
regulythe on-chain rules; kwota: "0" means not set
iletotal after the filter, for pagination
klopotwhat could not be read, or null

GET /api/moneta/{kroplomierz}#

One dropper, everything the coin page needs. The path parameter is the dropper address.

Same fields as one element of monety, plus:

FieldMeaning
czas, blokthe block all reads were pinned to; compute qualification against this time
blokKroplomierzathe dropper's deployment block, the start of every log scan; null when it could not be established (then klopot says why)
ostatniRuchunix time of the token's latest Transfer, from the block header; 0 = not established
bezruchPelnyfalse when the backwards scan did not find a transfer in its window; read ostatniRuch only when true

Statuses: 400 not an address, 404 not registered, 409 no registry on this chain, 502 the chain did not answer.

GET /api/migawka#

The holder snapshot of a token, built from Transfer logs and cached incrementally on the server's disk. This is the input of the split engine.

QueryMeaning
kroplomierzdropper address; the token is read from its token()
tokentoken address, for a coin without a dropper (the launch preview); mutually exclusive with kroplomierz
progholding threshold in base units for odKiedy; default 0. Use the coin's reguly.minSaldo to get the same streaks the keeper uses
swiezo1 to bypass the 20 s cache
{
  "token": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
  "decimals": 18,
  "symbol": "DEMO",
  "podaz": "1000000000000000000000000000",
  "doBloku": 63787176,
  "czas": 1789488594,
  "pelna": true,
  "prog": "0",
  "holdery": [
    { "adres": "0xcccc…", "saldo": "995983353632655580442562568", "odKiedy": 1789480218, "pierwszyRaz": 1789480218, "kontrakt": true },
    { "adres": "0xdddd…", "saldo": "805189751761844465545842", "odKiedy": 1789480219, "pierwszyRaz": 1789480219, "kontrakt": false }
  ],
  "msTrwania": 447
}
FieldMeaning
doBloku, czasthe snapshot block and its header time
pelnathe sum of balances equals totalSupply at doBloku; when false the index has a gap and every derived number is a lower bound
progthe threshold the streaks were computed with
holdery[].saldobalance at doBloku
holdery[].odKiedysince when the balance has continuously been ≥ prog; 0 = no streak
holdery[].pierwszyRazfirst time the address received the token; display only
holdery[].kontraktthe address had code
msTrwaniahow long the scan took

Holders are sorted by balance descending. The first full scan of a busy token takes seconds; later calls extend the cache from where it stopped. A snapshot for token uses the same cache as one for any dropper of that token.

To reproduce the coin page's qualifies / reason column, feed this response and the coin's reguly to the split engine with now = czas.

GET /api/historia#

Every drop of a dropper, oldest first, with the dropper's balance after each one.

QueryMeaning
kroplomierzdropper address (required on a public instance)
{
  "krople": [
    {
      "epoka": 1,
      "odbiorcow": 5,
      "suma": "235436799999999999999997",
      "saldoPo": "941747200000000000000003",
      "skrotListy": "0x…",
      "skrotRegul": "0x…",
      "domkniecie": true,
      "blok": 63753710,
      "czas": 1789485157,
      "tx": "0x…"
    }
  ],
  "odBloku": 63726100,
  "doBloku": 63787181,
  "pelna": true,
  "powod": null
}

One element per Kropla event, i.e. per batch; an epoch with several batches appears several times with the same epoka. saldoPo is reconstructed backwards from the live balance using Kropla, Zasilenie and Wyplata events, so it is exact for the latest drop and an approximation before any funding done with a plain transfer or through a fee-on-transfer token. pelna: false with powod means the scan could not cover the dropper's whole life.

GET /api/dostalem#

What one wallet actually received from one dropper, counted from the token's Transfer events (from = dropper, to = wallet), so fee-on-transfer tokens are reported as delivered, not as intended.

QueryMeaning
kroplomierzdropper address
adreswallet address
{ "suma": "47196412821206234847704", "wyplat": 5, "ostatnia": 1789485741, "pelna": true, "powod": null }

ostatnia is the header time of the latest payout, 0 when none. With pelna: false the sum is a lower bound.

GET /api/moje-tokeny#

Tokens a wallet currently holds, discovered from Transfer logs to that wallet and confirmed with live balanceOf. Powers the coin picker on the launch screen.

QueryMeaning
adreswallet address
swiezo1 to bypass the 15 s cache
{
  "tokeny": [
    {
      "adres": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
      "symbol": "DEMO",
      "nazwa": "Demo coin",
      "decimals": 18,
      "saldo": "1177184033103841900617129",
      "podaz": "1000000000000000000000000000",
      "udzialWPodazy": "0.001177184033103841",
      "maKroplomierz": true,
      "kroplomierz": "0x1234567890abcdef1234567890abcdef12345678",
      "logo": "ipfs://…"
    }
  ],
  "doBloku": 63787000,
  "odBloku": 63500000,
  "pelna": false,
  "czas": 1789488500,
  "blok": 63787000,
  "klopot": 0
}

The scan looks at a window of recent blocks per request (a filter without an address is limited to about 40 000 blocks per query on this chain) and extends the window on each request; odBloku/pelna say how far it got. A token that answers neither symbol() nor decimals() is dropped and counted in klopot. Sorted by share of supply, descending.

POST /api/rpc#

A JSON-RPC proxy for the app's own browser code, so that the server's RPC endpoint never reaches the client. It accepts single requests and batches of up to 50, read-only methods only:

eth_chainId, eth_blockNumber, eth_call, eth_getCode, eth_getBalance, eth_getLogs, eth_getBlockByNumber, eth_getTransactionByHash, eth_getTransactionReceipt, eth_getTransactionCount, eth_estimateGas, eth_gasPrice, eth_feeHistory, eth_maxPriorityFeePerGas, net_version.

eth_sendRawTransaction is refused with -32601: wallets broadcast through their own provider. Limits: 120 requests per 10 s per IP, 256 KB body, 20 s upstream timeout, no CORS headers. Upstream error messages are rewritten so they never reveal the endpoint. This route is not meant as a public RPC; point your own software at the chain's RPC.

GET /api/obraz?cid=<CID[/path]>#

IPFS image proxy for token logos. Accepts a CID (base58 CIDv0 or base32 CIDv1) with an optional path, tries a short list of gateways with a cool-off on failure, caches on disk, returns image/* only, at most 4 MB, with a sandboxing Content-Security-Policy. It never fetches arbitrary URLs: the caller chooses the content hash, the server chooses where to fetch it from. https:// logos are loaded by the browser directly.

Internal routes#

GET /api/stan and POST /api/wdrozenie serve the legacy single-deployment setup screen (/start) used by the local development environment. They are not part of the public API and may change without notice.

Examples#

S=https://<host>

# all dripping coins, first page
curl -s "$S/api/monety" | jq '.monety[] | {symbol, kroplomierz, saldo, doNastepnej}'

# one coin, its rules and the block it was read at
curl -s "$S/api/moneta/0x1234567890abcdef1234567890abcdef12345678" | jq '{symbol, czas, blok, reguly}'

# holders with the coin's own minimum balance as the streak threshold
curl -s "$S/api/migawka?kroplomierz=0x1234567890abcdef1234567890abcdef12345678&prog=0" | jq '.holdery | length'

# what a wallet received
curl -s "$S/api/dostalem?kroplomierz=0x1234…5678&adres=0xabcd…ef01" | jq
// Recompute the coin page's qualification column in your own code
const coin = await (await fetch(`${S}/api/moneta/${dropper}`)).json()
const snapshot = await (await fetch(`${S}/api/migawka?kroplomierz=${dropper}&prog=${coin.reguly.minSaldo}`)).json()

// the engine's input shapes keep the API's field names
const holders = snapshot.holdery.map((h) => ({ adres: h.adres, saldo: BigInt(h.saldo), odKiedy: h.odKiedy, kontrakt: h.kontrakt }))
const rules = {
  kwota: BigInt(coin.reguly.kwota),                                                        // amount per drop
  podzial: coin.reguly.porowno ? 'porowno' : 'proporcjonalnie',                            // equal or proportional
  minSaldo: BigInt(coin.reguly.minSaldo),                                                  // minimum balance
  minCzasTrzymania: coin.reguly.minCzasTrzymania,                                          // minimum holding time
  sufitNaAdres: coin.reguly.sufitNaAdres === '0' ? null : BigInt(coin.reguly.sufitNaAdres), // per-wallet cap
  pomijajKontrakty: coin.reguly.pomijajKontrakty,                                          // skip contracts
  wykluczone: coin.reguly.pomijajWlasne ? [coin.wlasciciel, coin.wykonawca] : [],         // excluded wallets
}
const result = policz({ czas: snapshot.czas, holdery: holders }, rules, snapshot.czas, { kroplomierz: dropper })
// result.przydzialy = allocations, result.odrzuceni = rejected with reasons, result.reszta = remainder