`bounties(uint256)` returns eighteen components. This board declared thirteen, in an order that predates the reward asset being split from the stake asset. Every component is a static type, so the short list did not revert: it read thirteen words off the front of the answer and named them by the wrong list. Position 1 is `rewardKind`, an enum in 0..3, which came back as `token: 0x00…03`, and every address after it moved up one slot — funder read as approver, approver as arbiter, worker as a deadline. The board rendered, and credited the wrong people. The events were stale in the same way. `BountyProposed` carries ten parameters, not seven: `rewardKind`, `rewardTokenId` and `stakeToken` joined it when the assets were split, and with a seven-parameter fragment `issueRef` — the only dynamic member, so the one whose offset moves — decoded to noise. `BountyFunded` and `BountyCancelled` name their third parameter `reward`. `DisputeResolved`, `StakeSlashed` and `RewardRecovered` were not declared at all; `ActivityKind` already had names for two of them, so the activity log had been promising steps it could not read. All twelve lifecycle events are now read. The single `token` field is gone. A reward is `rewardKind` / `rewardToken` / `rewardTokenId` / `reward` and a stake is `stakeToken` / `stake`, because a reward may be an NFT while the worker's stake stays fungible. Both are read through one `Asset`, so the board can show a stake beside a reward without a second formatter. `parseReward` inferred the reward kind from whether the token address was zero; the kind is recorded now, so the inference is deleted rather than kept as a fallback, and `RewardType` takes the contract's name for it, `AssetKind`. `Task` stops carrying a derived copy of the reward: the render site reads it off the struct. `src/abi.test.ts` pins the eighteen names and types, then encodes a bounty whose every field differs and decodes it back through the fragment, so the order is measured by what it does and not only by what it says. Swapping funder and approver, dropping a component, or restoring the seven-parameter `BountyProposed` each fail it. Fixture rebuilt to the eighteen-field shape — in the old order it would have enshrined the decode it was written under. Its Pars address and the README's address tables were the superseded V1 set, which answers a different struct; both now name the deployment in lp-0020. Co-authored-by: Hanzo Dev <[email protected]>
Work Board
White-label on-chain community work platform — a DAO's "Dework". It reads a live Bounty / Escrow / Reputation / Karma work-market directly over JSON-RPC (no subgraph) and renders the full Dework surface: Overview (org header + time-to- payment / total-paid stats + Bounties category grid), Board (To Do / In Progress / In Review / Done kanban), Open Tasks, Leaderboards (Top Contributors / Reviewers; Task Points = global Karma), Community Suggestions, Combined Board, per-Space tabs, a task detail with the real on-chain activity log + apply flow, and a cross-org Explore. The differentiator is portable reputation: per-DAO Reputation + global soul-bound Karma travel with the worker's address, not the platform. Mobile-first (390px up); every view also renders from a fixture with no wallet/chain, so it is always screenshot-able.
One repo, one Dockerfile. A build-time brand (VITE_BRAND, default zoo)
selects the white-label profile — chain, addresses, owner label, header/title,
and the RPC host baked into the CSP — from src/brands.ts:
| Brand | Chain | Site |
|---|---|---|
zoo |
200200 | https://work.zoo.network |
pars |
494949 | https://work.pars.network |
Run
npm install
npm run dev # http://localhost:5173 (proxies /rpc -> 127.0.0.1:9631 port-forward)
npm run build # tsc --noEmit && vite build -> dist/
npm test # vitest run
npm run preview # serve the production build
Point the dev proxy at any node instead of the local port-forward, and select a brand:
RPC_TARGET=https://api.pars.network npm run dev
VITE_BRAND=pars VITE_RPC_URL=https://api.pars.network/v1/chain/C/rpc npm run build
If the RPC is unreachable, the board renders a faithful fixture of that brand's live bounty #0 (behind the same render path) so the UI stays verifiable; the source badge shows FIXTURE.
Config
Brand-varying values live in one map: src/brands.ts; src/config.ts selects
the active brand from VITE_BRAND. To add a board, add a brand profile — it's a
data change, not a code change.
Deploy
CI (.github/workflows/docker.yml) builds on every push to main on luxfi's
in-cluster ARC pool (lux-build-amd64) and pushes to ghcr.io/luxfi/work-board
— never built locally. A brand matrix stamps one immutable tag per brand from
the same Dockerfile:
zoo→:sha-<commit>→ zoo-k8s, namespacezoo-mainnet(manifests ink8s/)pars→:pars-<commit>→ pars-k8s, namespacepars(manifests ink8s/pars/)
Each deployment.yaml + service.yaml runs 2 replicas serving dist on :3000,
Service :80 → :3000; pin the Deployment to the CI tag then kubectl apply. Each
route.yaml is the Traefik dynamic-config router (host → Service, TLS via the
ingress's built-in ACME resolver); hanzoai/ingress routes every public host
through a @file ConfigMap (zoo: zoo-system/zoo-dynamic; pars:
hanzo/pars-dynamic), not Ingress objects — merge the fragment in (see the
header in each route.yaml).
Data source
The live set, as recorded in lps/LPs/lp-0020-work-market.md and read by
src/brands.ts. The V1 addresses this table used to carry predate the split of
the reward asset from the stake asset and answer a different struct.
Zoo 200200
| Contract | Address |
|---|---|
| Bounty | 0x857c9fE5A644e048FEF7194Da10fFEb356a81fFD |
| Escrow | 0x5d9a3A8c1EeEfaC85178c5583425Ab25C0C208b3 |
| Reputation | 0xE6766AF6171F437D6d3952e73402b762718c1809 |
| Karma | 0xA8898F8573a463C167584979dc7E2cD43c24BAdF |
| owner | 0x229599f227231d8C90fcF1a78589F5DC4b7A6962 (Zoo DAO Safe) |
Pars 494949
| Contract | Address |
|---|---|
| Bounty | 0x79254D4A9286FBd65E7177440Be20f00934c33c2 |
| Escrow | 0x82323cf625C3a6DE0f5fB75A2e7bD41BB8200075 |
| Reputation | 0xD7fe65BCb3Efd85AC1E132E8b74365498E034f13 |
| Karma | 0x81fBD8287571E83D2a834Ae5A8B859092C45b2EA |
| owner | 0x4CEA4ac1C874a340B06e0422E77a477463C3a542 (Pars DAO Safe) |
bountyCount() + bounties(i) give the current struct — eighteen fields, with
the reward asset (rewardKind / rewardToken / rewardTokenId / reward) and
the stake asset (stakeToken / stake) recorded apart, so a reward can be an
NFT while the worker's stake stays fungible. The twelve lifecycle events give the
activity log, BountyProposed / WorkSubmitted give issueRef /
deliverableRef, and Reputation.completedOf / earnedOf give the worker's
score. ABIs are copied into src/abi.ts from the Foundry artifact (no cross-repo
import) and src/abi.test.ts pins the tuple order against it.