Operational TypeScript for cache.'s off-chain layer.
cron.ts # long-running scheduler (Railway entry point)
harvest.ts # one-shot: vault.harvest(amountIn) when USDC accumulates
register-seller.ts # one-shot: SIWE → list/reconcile Venice offers on Surplus
pricer.ts # one-shot: dynamic undercut of cheapest competitor per model
lib/
env.ts # required env-var validation
viem.ts # public + wallet clients (Base)
surplus.ts # Surplus API client (auth, listOffers, patchOffer, …)
npm install
cp .env.example .env # fill in OPERATOR_PRIVATE_KEY, VAULT_ADDRESS, VENICE_API_KEY, ...
# One-shot:
npm run harvest
npm run register-seller
npm run pricer
# Long-running (same thing Railway runs):
npm startSingle Railway service, single long-running Node process. cron.ts schedules:
harvest.ts— every 30 min by default (HARVEST_SCHEDULEenv to override, standard cron syntax, UTC)register-seller.ts— every Sunday at 08:00 UTC (RECONCILE_SCHEDULE)pricer.ts— every hour at :07 UTC (PRICER_SCHEDULE)
Plus one boot-time harvest.ts invocation per deploy/restart so the loop fires immediately on push (set BOOT_HARVEST=false to skip).
pricer.ts is the dynamic-pricing loop. Each hour it:
- Hits Surplus's public
GET /api/inference/markets(no auth) to readbest_*,direct_*,seller_count, andvolume_24h_usdper model. - For each of our active Venice offers, computes a target absolute price =
best_competitor × (1 − PRICER_UNDERCUT_FRAC), floored atdirect × PRICER_MIN_MULTIPLIER_FRAC. - PATCHes
price_input_per_1mandprice_output_per_1mvia the seller API.
Safety rails:
- Sole-seller skip — if we're the only seller (
seller_count <= 1) there's no competitor to undercut, so we keep whatever price is already set. - Floor — never goes below
PRICER_MIN_MULTIPLIER_FRAC × direct. - Churn band — if the new price is within
PRICER_CHURN_FRACof current, skip the PATCH. Makes back-to-back runs idempotent and prevents PATCH-spam on dead-volume models. - Rate limit — paced at ~28 PATCH/min (2.1s sleeps), under Surplus's 30/min offer-CRUD cap.
Note: dead-volume models (volume_24h_usd ≈ 0) are deliberately NOT skipped. Marginal cost of an active offer is essentially zero (we acquired the credit via DIEM stake; unused expires worthless), so we still want to be cheapest in case rare demand shows up. The churn band protects against pointless PATCH-spam on those offers.
Pricer is the sole authority on price. register-seller.ts no longer touches cost_multiplier or absolute price fields — so the two scripts can't fight. COST_MULTIPLIER is still used as the initial price at offer creation, then pricer takes over on its next tick.
To disable the pricer: set PRICER_SCHEDULE to a cron expression that never fires (e.g. 0 0 31 2 * — Feb 31 doesn't exist).
| Var | Default | Effect |
|---|---|---|
PRICER_SCHEDULE |
7 * * * * |
when to run (UTC cron) |
PRICER_UNDERCUT_FRAC |
0.05 |
how far below the cheapest competitor to price |
PRICER_MIN_MULTIPLIER_FRAC |
0.05 |
hard floor as a fraction of reference price |
PRICER_CHURN_FRAC |
0.02 |
skip patch when change is smaller than this |
cd scripts
railway login # if not already
railway init # create the project, link this directory
railway up # first deploy
# Set env vars (UI is easier than CLI for secrets):
# Dashboard → Project → Variables tab
# OPERATOR_PRIVATE_KEY (the keeper EOA — same wallet authorised on the vault)
# BASE_RPC_URL (private-mempool RPC recommended; Flashbots Protect or similar)
# VAULT_ADDRESS (after contract deploy)
# VENICE_API_KEY (from the ERC-1271 issuance flow)
# SURPLUS_BASE_URL = https://www.surplusintelligence.ai
# COST_MULTIPLIER = 0.3
# HARVEST_MIN_USDC = 10000000 (= $10; raise as vault scales)Once env is set Railway redeploys automatically. Watch railway logs to see the boot harvest fire.
Edit env vars in Railway dashboard, no redeploy needed for these:
| Var | Effect |
|---|---|
HARVEST_SCHEDULE |
cron expression for harvest cadence (default */30 * * * *) |
RECONCILE_SCHEDULE |
cron expression for offer reconciliation (default 0 8 * * 0 — Sunday 08:00 UTC) |
PRICER_SCHEDULE |
cron expression for the dynamic pricer (default 7 * * * * — hourly at :07) |
PRICER_UNDERCUT_FRAC / PRICER_MIN_MULTIPLIER_FRAC / PRICER_CHURN_FRAC |
pricer knobs — see Pricer section above |
HARVEST_MIN_USDC |
minimum vault USDC balance (microUSDC) before harvest fires |
BOOT_HARVEST |
set to false to skip the on-boot harvest call |
For scheduling changes you'll need to restart the service (railway restart or push a no-op commit).
- The operator private key in
OPERATOR_PRIVATE_KEYcontrols:harvest()(bounded grief: 1% slippage max per call)- SIWE auth to Surplus → can PATCH offer
payout_address(unbounded if compromised — full future-revenue leak)
- For v0 with 10 DIEM cap, Railway's env-var encryption-at-rest is acceptable. For meaningful TVL, move the key behind KMS (AWS, GCP, Turnkey).
- The Safe can rotate
keeperandauthorizedSignerat any time. If Railway is compromised, immediately rotate from the Safe and re-deploy with a fresh operator EOA. - See
../contracts/SECURITY.mdfor the full trust model.
railway logsshows each cron tick. Healthy state: harvest fires every 30 min, exits 0 with a "totalAssets after" line.- If you see repeated
harvest.ts exit=1, check the script's stderr in logs — most common: USDC below threshold (expected), invalid env var (fix in dashboard), insufficient ETH for gas (top up operator EOA). - The reconciler exits non-zero if any offer PATCH didn't take effect — that's a real alert. Check Surplus's status, then re-run manually.