DocsGuides

Self-hosting

Everything in the repository can be run by anyone: the contracts on any EVM chain, the web app against any registry, the keeper with any executor key. This page walks through a complete deployment. The examples assume Robinhood Chain; the code also knows a local Anvil chain (31337) for development.

Prerequisites#

ToolVersionUsed for
Node.js22.13 or newerweb app and keeper (--env-file, type stripping)
pnpm9web dependencies; the keeper reuses web/node_modules
Foundry (forge, cast, anvil)recentcontracts, tests, deployment
An archive RPC endpointthe holder index needs historical eth_getCode and full Transfer history; the chain's public RPC is not an archive node
git clone <repo> plop && cd plop
git submodule update --init --recursive     # OpenZeppelin and forge-std under kontrakty/lib
pnpm --dir web install

Checks#

cd kontrakty && forge test && cd ..         # contract tests
pnpm --dir web sprawdz                      # tsc --noEmit
pnpm --dir web test                         # node --test lib/*.test.mjs
node --test keeper/*.test.mjs

Contracts#

The ABI used by the app is generated from Foundry artifacts. After any change in kontrakty/src:

pnpm --dir web abi                          # forge build in kontrakty/, rewrites web/lib/abi.ts

Build from kontrakty/, not from the repository root: both directories carry a foundry.toml, and a drift between them would produce different bytecode.

Deploy the registry (once per chain)#

foundry.toml maps the RPC alias rh to the environment variable RH_RPC, so the endpoint never appears on a command line:

export RH_RPC="https://<your archive endpoint>"
cd kontrakty

# dry run: prints chain id, chain name, sender and balance, deploys nothing
forge script script/WdrozFabryke.s.sol:WdrozFabryke --rpc-url rh --sender <your address>

# for real, preferably from a hardware wallet
forge script script/WdrozFabryke.s.sol:WdrozFabryke --rpc-url rh --broadcast --ledger --sender <your address>

The script prints NEXT_PUBLIC_FABRYKA=0x… at the end. Fabryka has no constructor arguments. Optional environment variables let the same run register a dropper you deployed earlier (ZGLOS=<dropper> plus LOGO, KWOTA, MIN_SALDO, SUFIT_NA_ADRES, MIN_CZAS_TRZYMANIA, POROWNO, POMIJAJ_KONTRAKTY, POMIJAJ_WLASNE, OKRES_BEZRUCHU).

The registry cannot be moved once deployed. Read the chain line of the dry run before broadcasting.

Deploy a dropper by hand (optional)#

Droppers are normally created through Fabryka.utworz. kontrakty/script/Wdroz.s.sol deploys one directly with TOKEN, WYKONAWCA, MAKS_NA_EPOKE and MIN_ODSTEP; register it afterwards with zglos.

Web app#

Configuration#

web/.env.rh.local, written once by hand, chmod 600, ignored by git:

# server-side only: the archive endpoint. NO NEXT_PUBLIC_ prefix, it must never reach the browser.
RPC_URL=https://<your archive endpoint>

# optional on chain 4663: the plop registry and keeper are built into the app
# (`web/lib/wdrozenie.ts`, `ZNANE_WDROZENIA`). Set them only to point at your own contracts.
# NEXT_PUBLIC_CHAIN_ID=4663
# NEXT_PUBLIC_FABRYKA=0x…
# NEXT_PUBLIC_WYKONAWCA=0x…
# optional overrides
# NEXT_PUBLIC_EXPLORER=https://robinhoodchain.blockscout.com
# NEXT_PUBLIC_RPC=https://<a PUBLIC endpoint for the wallet's add-network prompt>
VariableScopeMeaning
RPC_URLserverendpoint for every server-side read; should be archive. Empty falls back to the chain's public RPC, which is not archive, so holder scans can come back incomplete
NEXT_PUBLIC_CHAIN_IDpublic4663 (default) or 31337
NEXT_PUBLIC_FABRYKApublicregistry address override; on 4663 the plop registry 0x1CE8…0502 is built in
NEXT_PUBLIC_WYKONAWCApublicdefault executor override; on 4663 the plop keeper is built in, on other chains empty means the creator's own wallet is proposed
NEXT_PUBLIC_EXPLORERpublicexplorer base URL for links
NEXT_PUBLIC_RPCpublicoverrides the chain's public RPC in the browser bundle; never a paid endpoint
UKRYTE_ADRESYservercomma-separated token or dropper addresses hidden on this instance; the constant list lives in web/lib/ukryte.ts

The browser never talks to RPC_URL. Client code sends JSON-RPC to the app's own /api/rpc, which forwards read-only methods and strips the endpoint from every error message.

Run#

pnpm --dir web dev:rh       # development server on 127.0.0.1:3500
pnpm --dir web build:rh     # production build
pnpm --dir web start:rh     # serve the build on port 3500

The :rh scripts load .env.rh.local explicitly via node --env-file and blank NEXT_PUBLIC_RPC unless you set it in the shell, so that a leftover .env.local from the local Anvil environment cannot leak the Anvil URL into a production bundle.

Runtime state lives in web/dane/ (ignored by git): the holder index cache under holdery/<chainId>/<token>.log, cached logos under obrazy/, wallet-token scan caches. Delete the directory to rebuild everything from chain.

Do not run next build while next dev is serving from the same .next directory. Stop the server, build, start.

Reverse proxy notes#

The rate limiter in /api/rpc is per process and trusts x-forwarded-for. Behind a load balancer with several instances, move the limit to the edge. The app sets no CORS headers; its JSON routes are same-origin by design. If you want third parties to consume them cross-origin, add CORS at the proxy for GET /api/* only.

Vercel#

The web app deploys to Vercel as a normal Next.js project. Set the project's Root Directory to web. On chain 4663 a project with no environment variables at all already serves the live plop registry, because the registry and keeper addresses are built in; the one variable worth adding is RPC_URL (server-side, an archive endpoint), without which holder scans run on the public RPC and can come back incomplete. NEXT_PUBLIC_* overrides are baked in at build time, so redeploy after changing them. Everything else is handled in code:

  • the holder index and the wallet scan write their caches to /tmp when VERCEL is set, because the app directory is read-only there; the cache lives as long as the function instance;
  • the log-scanning routes declare maxDuration = 60, because a first scan can exceed the default function timeout;
  • /docs is rendered at build time from the repository's docs/ folder, which Vercel includes because "Include source files outside of the Root Directory" is on by default.

The keeper is a long-running process and does not run on Vercel; run it on any machine with Node, as described below.

Keeper#

See Keeper and executor for what it does. To run it against your registry:

# keeper/.env.keeper, chmod 600, ignored by git
WYKONAWCA_KLUCZ=0x<executor private key>
WYKONAWCA_ADRES=0x<executor address>
(set -a; . keeper/.env.keeper; . web/.env.rh.local; set +a; node keeper/fabryka.mjs --sucho --raz)   # rehearse
(set -a; . keeper/.env.keeper; . web/.env.rh.local; set +a; \
  nohup node keeper/fabryka.mjs --wyslij --co 15 >> keeper/dane/fabryka.log 2>&1 &)                  # run

Put NEXT_PUBLIC_WYKONAWCA in the web configuration to the same address so the launch screen proposes your keeper. Fund the executor with ETH for gas only. Keep the owner keys of droppers elsewhere.

For a long-running service wrap the same command in systemd, launchd or a container; the keeper is idempotent across restarts thanks to its journal.

Local development environment#

web/script/dev.mjs starts Anvil on 127.0.0.1:8545, deploys Multicall3, the registry, several tokens (including a fee-on-transfer one) and about ten droppers in different states, writes web/.env.local and web/dane/wdrozenie.json, and prints what it made:

node web/script/dev.mjs            # or --od-nowa to wipe and recreate
pnpm --dir web dev                 # app on 127.0.0.1:3500 against Anvil

The single-dropper CLI reads that deployment file: node keeper/harmonogram.mjs stan, plan, kropla --wyslij with WYKONAWCA_KLUCZ set to the Anvil executor key the script printed.

Ports and processes#

Web app3500 (dev and production)
Anvil8545
Keeperno port; a loop that exits on SIGINT/SIGTERM after finishing the batch in flight

Upgrading contracts#

There is no proxy and no upgrade path by design. A new Kroplomierz version means new droppers; a new Fabryka means a new registry address in the web configuration and, if you want the old entries, zglos for each of them from their owners.