Skip to content

Latest commit

 

History

History
118 lines (87 loc) · 6.08 KB

File metadata and controls

118 lines (87 loc) · 6.08 KB

scripts/

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, …)

Local development

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 start

Production (Railway)

Single Railway service, single long-running Node process. cron.ts schedules:

  • harvest.ts — every 30 min by default (HARVEST_SCHEDULE env 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

pricer.ts is the dynamic-pricing loop. Each hour it:

  1. Hits Surplus's public GET /api/inference/markets (no auth) to read best_*, direct_*, seller_count, and volume_24h_usd per model.
  2. For each of our active Venice offers, computes a target absolute price = best_competitor × (1 − PRICER_UNDERCUT_FRAC), floored at direct × PRICER_MIN_MULTIPLIER_FRAC.
  3. PATCHes price_input_per_1m and price_output_per_1m via 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_FRAC of 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

One-time setup

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.

Tuning

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).

Security notes

  • The operator private key in OPERATOR_PRIVATE_KEY controls:
    • 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 keeper and authorizedSigner at any time. If Railway is compromised, immediately rotate from the Safe and re-deploy with a fresh operator EOA.
  • See ../contracts/SECURITY.md for the full trust model.

What to watch

  • railway logs shows 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.