RetroPick is an on-chain prediction market engine for Base Sepolia: one upgradeable MarketEngine hosts many markets (templates), each running epochs—open → lock → resolve → claim—with settlement driven by Chainlink-family oracles and optional trusted reporters. Operators define markets via upsertTemplate / initializeMarket; traders deposit stake on sides per epoch; winners claim after resolution; protocol fees route to treasury.
Problem addressed: Teams need a single protocol instance that supports many market shapes (direction, thresholds, ranges, composites, …), professional ops (templates, epochs, treasury), and flexible automation (manual keeper steps vs rolling rounds)—not one-off pools per market.
How it fits together (off-chain stack): This monorepo adds Solidity (package/contract), shared ABIs (package/abi), a Go API + indexer (apps/backend), the user app (apps/fe-v1), and an operator dashboard (apps/ops). Deep contract behavior—including modular MarketEngineDispatcher, oracle routing, checkpoints, and gas notes—is documented in package/contract/currentSmartContract.md.
Differentiators
- Modular UUPS engine: Admin, user, and claims hot paths on the dispatcher; lifecycle and views delegated to modules; one storage layout (
MarketEngineState). - Multi-adapter Chainlink routing: Templates select oracle class (price, rate, smart data, macro, equity) per
currentSmartContract.md§1.2. - Epoch modes: Manual epochs (discrete
openEpoch/lockEpoch/resolveEpoch) vs rolling automation where a keeper advances the pipeline on an interval—see the same doc for ops narrative (§0.3).
RetroPick combines:
- Smart contracts for market lifecycle, settlement, and claims.
- Go backend for API, indexer, funding flow, and realtime events.
- Frontend apps for users and operators.
At a high level:
- Operators create and initialize markets.
- Users trade during epoch open windows.
- Epochs lock and resolve via configured oracle routes.
- Winners claim and fees route to treasury.
- Optional funding abstraction lets users bridge/swap into settlement USDC balance.
For full protocol internals, see package/contract/currentSmartContract.md.
| Tool | Notes |
|---|---|
| Node.js | 20+ |
| pnpm | 10.x (see package.json packageManager) |
| Docker & Docker Compose | Recommended for Postgres + API + indexer + ops |
| Docker Buildx | Required by Compose v2 Bake builds |
| Go | 1.24+ optional when running backend on host |
From repo root:
pnpm installdocker compose up -d --buildIf Docker Desktop bridge cannot reach postgres:5432, use:
docker compose --env-file compose.desktop-hairpin.env up -d --buildIf backend need update, use:
pnpm docker:down && pnpm docker:up# User app
pnpm dev:fe-v1
# Operator app
pnpm dev:ops
# Docs app
pnpm dev:docscurl -sS http://127.0.0.1:8080/api/v1/health
curl -sS http://127.0.0.1:8080/api/v1/config/contractscurl -sS http://127.0.0.1:8080/api/funding/config
curl -sS http://127.0.0.1:8080/api/funding/intents/<intentId>
curl -sS http://127.0.0.1:8080/api/funding/executions/<executionId>Use the complete testnet funding smoke script in:
It covers intent creation, option selection, execution tracking, optional webhook hints, and final balance/intent polling.
- API is up:
/api/v1/healthreturnsok: true. - Contracts loaded:
/api/v1/config/contractsreturns expected chain and addresses. - Migrations applied: new funding tables/columns exist before testing abstraction flow.
- Workers running: credit, matcher, destination poller should be active in API logs.
- Funding endpoints return 404: backend process is not running latest code or not restarted after changes.
- No destination credits: verify
SETTLEMENT_*env vars, poller/matcher intervals, and destination transfer ingestion rows. - Docker Buildx warnings: install buildx plugin (see Troubleshooting section).
High-level protocol flow (deploy → template → epochs → settlement → treasury), aligned with §0.5 of currentSmartContract.md:
flowchart LR
subgraph deployBlock [Deploy]
adapters[OracleAdapters]
proxyNode[MarketEngineProxy]
wireup[ModuleWireup]
end
subgraph marketBlock [Market]
tmpl[upsertTemplate]
initMarket[initializeMarket]
end
subgraph epochBlock [Epochs]
adv[KeeperOrRolling]
trade[UsersDepositSwitch]
end
subgraph settleBlock [Settlement]
res[ResolveAndOracle]
cl[Claims]
end
subgraph treasBlock [Treasury]
feePool[FeeReserve]
wdraw[withdrawFees]
end
adapters --> proxyNode
wireup --> proxyNode
proxyNode --> tmpl --> initMarket --> adv
trade --> adv
adv --> res --> cl
res --> feePool --> wdraw
Conceptual epoch lifecycle (one round per template; exact rules vary by MarketType). Users typically deposit or switch sides while the epoch is open (and where allowed before lock); manual vs rolling differs in how the admin/keeper advances open/lock/resolve:
flowchart LR
openPh[Open]
lockPh[Lock]
resolvePh[Resolve]
claimPh[Claim]
openPh --> lockPh --> resolvePh --> claimPh
The canonical MarketType set has nine variants: Direction, Threshold, RangeClose, Velocity, Ladder, Convergence, Composite, Corridor, Cascade. For guarded launches, Direction / Threshold / RangeClose are the lowest operator burden; Convergence / Composite / Corridor / Cascade need the most oracle and incident discipline. Full approval guidance is in package/contract/.operator/.marketType.md.
- Fund exploration from
TokenFauceton Base Sepolia (address below; explorers inpackage/abi/address.md). - Run
docker compose uppluspnpm dev:fe-v1/pnpm dev:ops(see below) so judges see indexer-backed UI against the deployed engine. - Deep operator flows (
upsertTemplate,initializeMarket, epoch actions):package/contract/.operator/.runbook.md.
Chain id 84532. Use the MarketEngine proxy for all routine reads and writes; implementation addresses are for verification/debugging only (address.md notes).
| Role | Address |
|---|---|
MarketEngine proxy (ERC1967) |
0x1ed89defc8fbcbd512c562b148868ffdc778018a |
Implementation (MarketEngineDispatcher) |
0xf8b69b881fb35feb804cfec761fdeb88c4e45ef1 |
| TokenFaucet | 0xf6c1b6bddd06972f08772de7954432e10c853231 |
MockERC20 stake (mSTK, 18 decimals) |
0xb7f49377af6adbef64f513cf04dbdac9d0af01b1 |
Oracle adapters (ChainlinkAdapter, Rate, SmartData, Macro, Equity) and lifecycle modules (View, CoreLifecycle, RollingLifecycle, Admin, UserOpsClaims) — full list with Basescan / Blockscout links: package/abi/address.md.
Apps consume the same addresses via packages/contracts/registry.base-sepolia.json. After changing deployments, regenerate:
pnpm contracts:registry| Tool | Notes |
|---|---|
| Node.js | 20+ |
| pnpm | 10.x (see package.json packageManager) |
| Docker & Docker Compose | Recommended for Postgres + API + indexer + ops |
| Docker Buildx | Compose v2 uses Buildx Bake for builds; install the Buildx CLI plugin so docker buildx version works and you avoid Compose bake warnings. On Ubuntu/WSL, apt-get install docker-buildx-plugin only appears after Docker’s APT repo is configured (see Troubleshooting). |
| Go | 1.24+ optional, only if you run apps/backend on the host |
From the repository root:
pnpm installWorkspace packages are declared in pnpm-workspace.yaml (apps/*, packages/*). Use pnpm from the repo root; plain npm install inside a workspace package can fail on workspace:* references.
Build and start Postgres, API, indexer, and the operator UI:
docker compose up -d --buildIn-stack DATABASE_URL defaults to postgres:5432 (Compose service DNS). That matches Linux Docker Engine, WSL, and many setups.
If retropick-migrator waits ~5 minutes then exits 1 while Postgres is healthy (Docker Desktop userland bridge cannot reach postgres:5432), start with the host-published port:
docker compose --env-file compose.desktop-hairpin.env up -d --build(compose.desktop-hairpin.env sets host.docker.internal:5433; extra_hosts: host.docker.internal:host-gateway is already in docker-compose.yml so that name resolves on engines that do not define it—fixing no such host.)
Shortcuts: pnpm docker:up, pnpm docker:up:desktop, pnpm docker:down, pnpm docker:build. Image builds use BuildKit by default (expects DOCKER_BUILDKIT=1) for layer and cache mounts in Dockerfiles.
| Service | Host port | Purpose |
|---|---|---|
| Postgres | 5433→5432 |
Published on host 5433 (container still listens on 5432); avoids host conflicts on 5432 |
| API | 8080 |
REST + WebSocket; migrations run in cmd/api on startup |
| Indexer | — | Follows Base Sepolia via RPC_URL |
| Ops | 3001 |
Next.js operator UI |
Compose defaults align with 84532: RPC_URL=https://sepolia.base.org. In-stack DATABASE_URL defaults to postgres:5432; use compose.desktop-hairpin.env (or RETROPICK_COMPOSE_DATABASE_URL) when the bridge path to Postgres times out. Host tools use 127.0.0.1:5433.
curl -sS http://127.0.0.1:8080/api/v1/health
curl -sS http://127.0.0.1:8080/api/v1/config/contractsUse this script to smoke-test the target-amount funding abstraction flow on testnet once the API is running with latest migrations and env vars (SETTLEMENT_*, LIFI_*, poll/matcher intervals, etc.).
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
WALLET="${WALLET:-}"
TARGET_AMOUNT="${TARGET_AMOUNT:-25.00}"
WEBHOOK_SECRET="${LIFI_WEBHOOK_SECRET:-}"
CLIENT_NONCE="${CLIENT_NONCE:-smoke-$(date +%s)}"
if [[ -z "$WALLET" ]]; then
echo "Set WALLET=0x... before running."
exit 1
fi
echo "== Health and contracts =="
curl -fsS "$BASE_URL/api/v1/health" | jq .
curl -fsS "$BASE_URL/api/v1/config/contracts" | jq .
echo "== Funding config =="
curl -fsS "$BASE_URL/api/funding/config" | jq .
echo "== Create intent =="
CREATE_RESP="$(curl -fsS -X POST "$BASE_URL/api/funding/intents" \
-H "Content-Type: application/json" \
-d "{
\"userAddress\":\"$WALLET\",
\"targetCurrency\":\"USD\",
\"targetAmount\":\"$TARGET_AMOUNT\",
\"clientNonce\":\"$CLIENT_NONCE\",
\"mode\":\"AUTO_BEST_SOURCE\"
}")"
echo "$CREATE_RESP" | jq .
INTENT_ID="$(echo "$CREATE_RESP" | jq -r '.intentId')"
echo "== Scan balances / build options =="
curl -fsS -X POST "$BASE_URL/api/funding/intents/$INTENT_ID/scan-balances" \
-H "Content-Type: application/json" \
-d '{}' | jq .
echo "== Fetch options =="
OPTIONS_RESP="$(curl -fsS "$BASE_URL/api/funding/intents/$INTENT_ID/options")"
echo "$OPTIONS_RESP" | jq .
OPTION_ID="$(echo "$OPTIONS_RESP" | jq -r '.recommendedOptionId // (.options[0].optionId // empty)')"
if [[ -z "$OPTION_ID" ]]; then
echo "No funding option available."
exit 1
fi
echo "== Select option =="
SELECT_RESP="$(curl -fsS -X POST "$BASE_URL/api/funding/intents/$INTENT_ID/select-option" \
-H "Content-Type: application/json" \
-d "{\"optionId\":\"$OPTION_ID\"}")"
echo "$SELECT_RESP" | jq .
EXECUTION_ID="$(echo "$SELECT_RESP" | jq -r '.execution.executionId')"
echo "== Fetch execution payload (canonical route) =="
curl -fsS "$BASE_URL/api/funding/executions/$EXECUTION_ID" | jq .
echo
echo "Manual wallet step required next:"
echo "1) Execute the serialized route from FE/wallet."
echo "2) Capture source tx hash as SOURCE_TX_HASH."
echo
read -r -p "Enter SOURCE_TX_HASH (0x...) to continue, or Ctrl+C to stop: " SOURCE_TX_HASH
echo "== Mark execution started =="
curl -fsS -X POST "$BASE_URL/api/funding/executions/$EXECUTION_ID/start" \
-H "Content-Type: application/json" \
-d "{\"walletAddress\":\"$WALLET\",\"clientRouteExecutionId\":\"$(uuidgen)\"}" | jq .
echo "== Submit source tx =="
curl -fsS -X POST "$BASE_URL/api/funding/executions/$EXECUTION_ID/source-tx" \
-H "Content-Type: application/json" \
-d "{\"chainId\":84532,\"txHash\":\"$SOURCE_TX_HASH\"}" | jq .
echo "== Route update hint =="
curl -fsS -X POST "$BASE_URL/api/funding/executions/$EXECUTION_ID/route-update" \
-H "Content-Type: application/json" \
-d "{
\"status\":\"BRIDGING\",
\"observedTxHashes\":[{\"chainId\":84532,\"txHash\":\"$SOURCE_TX_HASH\",\"stepIndex\":0,\"type\":\"SOURCE\"}]
}" | jq .
if [[ -n "$WEBHOOK_SECRET" ]]; then
echo "== Optional LI.FI webhook hint =="
curl -fsS -X POST "$BASE_URL/api/funding/webhooks/lifi" \
-H "Content-Type: application/json" \
-H "X-Lifi-Webhook-Secret: $WEBHOOK_SECRET" \
-d "{
\"eventId\":\"smoke-$EXECUTION_ID\",
\"eventType\":\"ROUTE_UPDATE\",
\"executionId\":\"$EXECUTION_ID\",
\"status\":\"BRIDGING\",
\"sourceTxHash\":\"$SOURCE_TX_HASH\"
}" | jq .
fi
echo "== Poll intent + balance =="
for i in $(seq 1 20); do
INTENT_RESP="$(curl -fsS "$BASE_URL/api/funding/intents/$INTENT_ID")"
STATUS="$(echo "$INTENT_RESP" | jq -r '.status')"
echo "intent status: $STATUS"
if [[ "$STATUS" == "CREDITED" || "$STATUS" == "MANUAL_REVIEW" || "$STATUS" == "FAILED" ]]; then
break
fi
sleep 3
done
curl -fsS "$BASE_URL/api/users/$WALLET/balance" | jq .
cat <<'EOF'
Expected DB progression:
- destination_usdc_transfers: UNMATCHED -> matched_* populated -> CREDITED (or AMOUNT_TOO_LOW)
- funding_executions: source_tx_hash/destination_tx_hash/provider_status updated
- funding_intents: BRIDGING -> CREDITED (or MANUAL_REVIEW/FAILED)
- user_balances + balance_ledger updated on credit
EOFNotes:
- The script uses
jqanduuidgen; install them if missing. - Replace
chainIdinsource-txpayload if your source chain differs from84532. - Webhook is optional and non-authoritative; credit still requires verified destination transfer ingestion.
Stop containers (docker compose down; add -v to remove the Postgres volume).
| File | Use |
|---|---|
.env.example |
Host-run Go API / indexer (DATABASE_URL, PORT, RPC_URL, …) |
compose.desktop-hairpin.env |
RETROPICK_COMPOSE_DATABASE_URL → host.docker.internal:5433 when postgres:5432 from containers fails |
docker-compose.yml |
Container defaults; optional RETROPICK_COMPOSE_DATABASE_URL interpolation |
apps/fe-v1/.env.local.example |
NEXT_PUBLIC_API_URL, optional NEXT_PUBLIC_DOCS_URL, and NEXT_PUBLIC_REOWN_PROJECT_ID (Reown / WalletConnect) for the user app |
apps/ops/.env.local.example |
NEXT_PUBLIC_API_URL for the operator UI |
package/contract/.env.example |
Foundry / deployment scripts |
Do not commit real .env files (see root .gitignore).
Watchlist mutations: POST /api/v1/user/watchlist accepts JSON with wallet, action, and templateId / templateIds only (no signature). Note: any client can send any wallet address; protect the API with network policy or add your own auth if you expose it publicly.
Ensure the API is reachable (default http://127.0.0.1:8080).
# User app — http://localhost:3000
pnpm dev:fe-v1
# Operator UI — prefers port 3001 (see apps/ops/scripts/dev.mjs)
pnpm dev:ops
# Docs — http://localhost:3002/docs
pnpm dev:docsIf the API is not on 127.0.0.1:8080, set NEXT_PUBLIC_API_URL for apps/fe-v1 and apps/ops (browser-side requests). If docs are not on localhost:3002, set NEXT_PUBLIC_DOCS_URL for apps/fe-v1. Set NEXT_PUBLIC_REOWN_PROJECT_ID to override the bundled Reown (WalletConnect Cloud) project id; in the Reown Dashboard enable Google under Email & Socials and add your app origins (e.g. http://localhost:3000) under allowed domains.
Use a wallet on Base Sepolia with test ETH/USDC per your deployment.
Step-by-step walkthrough (why Vercel and the API are separate, env vars, CORS, smoke tests): docs/vercel-and-api-deployment.md.
End-to-end tutorial for hosting only the Go API on Vercel (apps/backend, cmd/api): docs/vercel-backend.md (Postgres + migrator off-Vercel; indexer needs another host).
Deploy the browser apps and Go backend as separate services:
| Service | Recommended target | Notes |
|---|---|---|
| User app | Vercel project rooted at apps/fe-v1 |
Build command: cd ../.. && pnpm --filter fe-v1 build |
| Docs | Vercel project rooted at apps/docs |
Build command: cd ../.. && pnpm --filter docs build |
| API | Container host from apps/backend/Dockerfile with SERVICE=api |
Expose HTTPS at a public API domain |
| Indexer | Container host from apps/backend/Dockerfile with SERVICE=indexer |
Long-running worker; no public port required |
| Migrator | Release/predeploy job with SERVICE=migrator |
Run before API/indexer start after schema changes |
| Postgres | Managed Postgres | Use provider-required SSL settings, commonly sslmode=require |
Vercel only deploys the selected root directory. A project rooted at apps/fe-v1 will not run apps/backend or apps/docs, so production must not rely on local fallbacks like 127.0.0.1:8080 or localhost:3002.
Set these Vercel variables on the apps/fe-v1 project:
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_DOCS_URL=https://docs.example.com/docs
NEXT_PUBLIC_REOWN_PROJECT_ID=d2f5d76bb1b000f9443e2172d3a560baSet these variables on the backend API service:
PORT=8080
DATABASE_URL=postgres://USER:PASSWORD@HOST:5432/DB?sslmode=require
RPC_URL=https://sepolia.base.org
CORS_STRICT=1
CORS_ALLOWED_ORIGINS=https://app.example.com,https://docs.example.com
# Optional for Vercel preview deployments:
CORS_ALLOWED_ORIGIN_PATTERNS=https://*.vercel.appSmoke-test production after deploy:
curl -sS https://api.example.com/api/v1/health
curl -sS https://api.example.com/api/v1/markets-
Postgres up:
docker compose up -d postgres -
Export vars (see
.env.example) or:export DATABASE_URL='postgres://retropick:retropick@127.0.0.1:5433/retropick?sslmode=disable' export PORT=8080 export RPC_URL=https://sepolia.base.org
-
From
apps/backend:GOTOOLCHAIN=auto go run ./cmd/api
Live operator RPC routes (/api/v1/ops/live/*) need RPC_URL and ABIs under apps/backend/internal/abis/ (aligned with package/abi).
# All workspace packages that define a test script (frontend Vitest today)
pnpm test
# Explicit targets
pnpm --filter fe-v1 test
cd apps/backend && make test # go test ./...WSL2 + Docker: If logs show DNS errors (lookup postgres on 127.0.0.11:53) or migrations flap while Postgres starts, confirm Docker Desktop is running, wait until Postgres is ready, or adjust Docker DNS if a VPN breaks embedded resolution.
E: Unable to locate package docker-buildx-plugin / Compose “build using Bake, but buildx isn’t installed”: The APT package exists in Docker’s package repository (download.docker.com), not in Ubuntu’s default indexes alone. Either:
-
Add Docker Engine’s repo for your distro (Ubuntu, Debian), run
sudo apt-get update, thensudo apt-get install docker-buildx-plugin, or -
Install the CLI plugin binary for your architecture (often
amd64on Windows/WSL on Intel):
mkdir -p ~/.docker/cli-plugins
VERSION=v0.32.1
case "$(uname -m)" in
x86_64|amd64) SUFFIX=linux-amd64 ;;
aarch64|arm64) SUFFIX=linux-arm64 ;;
*) echo "unsupported CPU: $(uname -m)"; exit 1 ;;
esac
curl -fsSL "https://github.com/docker/buildx/releases/download/${VERSION}/buildx-${VERSION}.${SUFFIX}" -o ~/.docker/cli-plugins/docker-buildx
chmod +x ~/.docker/cli-plugins/docker-buildx
docker buildx versionBump VERSION periodically from buildx releases.
RETRODEPLOYER is the single entry point for RetroPick operator workflows: it builds calldata via the local API (POST /tx/prepare and related routes), then broadcasts with Foundry cast send using scripts under package/contract/scripts/market/. It does not duplicate HTTP/RPC logic—the repo-root wrapper delegates there.
| Requirement | Why |
|---|---|
| Run from the monorepo root | Paths assume $V1_ROOT; cd package/contract and running a different ./scripts/ will not work as documented. |
| Go API up | Prepare flows need API_URL (default http://127.0.0.1:8080). Start Docker Compose (or host API) before prepare / deploy-all. |
package/contract/.env |
Copy from package/contract/.env.example. At minimum set RPC_URL, CAST_ACCOUNT (Foundry keystore account name), and optionally ETH_PASSWORD or your usual Foundry password setup. API_URL overrides the default if your API is not on localhost:8080. |
Upsert fixtures live under package/contract/.operator/upsert-params/ (override with UPSERT_DIR). Calldata always comes from the API before on-chain broadcast—same rule as deployment scripts in package/contract/scripts/market/broadcast-prepared-ops-tx.sh (explicit nonce / precheck behavior is implemented there).
# Full help (authoritative list of subcommands and env vars — use this first)
./scripts/RETRODEPLOYER help
# Same entry point
bash ./scripts/RETRODEPLOYER help
pnpm run retropick:deployer -- help
# Interactive menu (no subcommand defaults to menu)
./scripts/RETRODEPLOYER
pnpm run retro
# Convenience: deploy-all alias
pnpm run retropick:deployShorthands ./scripts/retro, pnpm run retro, and menu option ./scripts/retro d mirror deploy-all (nine manual-type fixtures, keystore-driven flow—see help output).
Use ./scripts/RETRODEPLOYER help for exact flags; this table is an index.
| Area | Examples |
|---|---|
| Prepare | prepare upsert (manual type 1–9, 01–09, or path to JSON), prepare fn, prepare all, prepare all-to <dir> |
| Send | send with a prepared JSON path or stdin; send last-lock, last-resolve, last-activate pick the newest /tmp/retropick-*.json |
| Batch | batch-send <dir> (optional -y, --resume, --manifest, retries) |
| Full pipeline | deploy-all (types, steps, resume, dry-run—see help) |
| Launch | launch with optional open / --open-at / --lock-at / --resolve-at |
| Epoch control | activate-epoch, advance-epoch (lock → resolve → open; --fast for short dev windows), prepare-lock-epoch, prepare-resolve-epoch, prepare-open-all, resume-open |
| Feeds / oracle | feeds discover, feeds auto-assign, feeds fix-adapter |
| Recovery | auto-deploy, recover-feed-drift; aliases recover-stuck-epoch / recover |
| Monitoring | monitor overview, monitor trade-ready, monitor templates, monitor global, monitor incidents, … |
| Product | frontend-visibility (hide, unhide, list) — toggles indexer/API visibility for discover |
| Emergency (prepare-only) | emergency prepare … (pause, halt rolling, cancel epoch, …) — inspect JSON then send |
Loaded from package/contract/.env (see help for the full set):
| Variable | Role |
|---|---|
API_URL |
Backend for calldata preparation (default http://127.0.0.1:8080). |
RPC_URL |
JSON-RPC for cast send / read prechecks. |
CAST_ACCOUNT |
Foundry keystore account name for broadcasts. |
ETH_PASSWORD |
Optional path to one-line password file for Foundry. |
RETRODEPLOYER_* |
Timeouts, work dirs, retries, --fast epoch windows, index wait after txs, NO_COLOR, etc.—see help. |
- On-chain errors (e.g.
No access,TooEarlyToResolve):broadcast-prepared-ops-tx.shprints hints; feed gating on Base Sepolia is a common cause—follow thefeeds discover→fix-adapter→ re-upsert path inhelpand in the operator runbook. - Authoritative detail: always run
./scripts/RETRODEPLOYER helpand keep.operator/runbook.mdopen for end-to-endupsertTemplate→initializeMarket→ epoch procedures.
- Operator flows:
package/contract/.operator/.runbook.md - Integration specs and ABI mapping:
.dev/