Skip to content

Economy Service

Status: Economy V1 (Audition) shipped; Economy V2 (Competition tickets and settlement) pending.

Related: Clash Of Clones Product Spec, Capacity And Model Tiers, Clone Service Module, Economy Module, Backend Overview

Scope

EconomyService owns money-shaped facts. Economy V1 (Audition) owns audition deposits, purchases, and entitlement grants. Economy V2 (Competition) will own ticket sales, pool snapshots, settlement events, payout signatures, creator royalties, and protocol rake. Clone Service owns the projection (effective entitlement) that AI/Prop guards read.

text
EconomyService (facts)        Clone Service (projection)
  ├─ deposits/payments          └─ effective entitlement row
  ├─ payouts                        (UI + AI + Prop guards)
  ├─ entitlement grants
  ├─ tickets / pools
  ├─ payout signatures
  └─ settlement events

On-chain Contract

The economy module talks to a SignedVault contract deployed on MegaETH. SignedVault is a resolver-mediated escrow:

  • Users deposit ETH (or ERC20 with Permit2) tagged with (resolver, nonce).
  • Withdrawals require an EIP-712 signature from resolver; the backend signs off-chain and the user broadcasts the withdrawal.
  • resolverBalanceOf[resolver][token] caps total outflow per resolver, so a compromised resolver key cannot drain more than that resolver's deposits.
  • Pump Party uses one dedicated pump_party resolver address. Other dApps can share the same vault with their own resolver — independent ledgers per resolver.

Contract source lives at contracts/SignedVault.sol. The backend integrates via ABI + address only.

Deposit (income) Flow

text
1. Frontend wallet
     SignedVault.depositETH(resolverAddr, nonce) { value: feeWei }
2. Frontend → backend
     POST /api/v1/economy/audition-fee/submit
        { depositNonce, tier, assetCapacity, modelStrength, cloneId? | createClone? }
3. Backend
     - readContract: getDeposit(user, ETH, resolver, nonce) → wei
     - Reject if wei == 0 or wei != expectedFeeWei
     - Insert economy_purchases row (UNIQUE on (resolver_address, deposit_nonce))
     - Call clone module → upsert clone_capacity_entitlements
       (source='audition_fee', economyEventId=purchase id)
4. Response
     - 200 with purchase + entitlement snapshot
     - 409 if nonce already redeemed
     - 422 if on-chain amount mismatch

The frontend chooses nonce; the backend uses (resolver, nonce) for idempotency. A submit retry with the same nonce after success returns the existing purchase + entitlement (idempotent).

Withdrawal Signature Flow

Withdrawal triggers are owned by future settlement code, but the signing helper lives in the Economy module so race tickets, creator royalties, and protocol rake can reuse it.

text
1. Lifecycle event (e.g. race settled)
     -> calls economy.withdrawalSigner.sign({ user, amountWei, kind })
2. Backend
     - Generate EIP-712 nonce (160-bit address || 64-bit ms timestamp || 32-bit random)
     - Sign Withdraw(user, ETH, amount, resolver, nonce, deadline) with resolver key
     - Persist signature on the relevant row (payout / royalty)
3. Frontend (later)
     - Reads signature → calls SignedVault.withdrawETH(...) from user wallet
     - Contract verifies signature, marks nonce used, transfers ETH

v1 implements audition-fee deposit verification. Race ticket payouts and creator/protocol payouts should use the withdrawal signer and add product-specific ledger rows.

Economy V1 Audition Fee Pricing

Matrix from the product spec:

Model strength1-3 assets4-6 assets7-10 assets
cheap (Haiku)0.01 ETH0.015 ETH0.02 ETH
medium (Sonnet)0.02 ETH0.03 ETH0.04 ETH
best (Opus)0.04 ETH0.06 ETH0.08 ETH

Pricing is committed in backend/src/modules/economy/pricing/audition-fee.ts. Treat it as a tunable constant, not a feature flag — adjust by editing code + bumping the PR.

Entitlement Mapping

A confirmed audition-fee purchase grants exactly one entitlement row via the existing Clone Service sync endpoint:

text
tier              = 'starter' | 'builder' | 'pro'   # derived from asset capacity
maxActiveAssets   = chosen capacity (1-10)
allowedModelStrength = 'cheap' | 'medium' | 'best'   # derived from model tier
source            = 'audition_fee'
economyEventId    = economy_purchases.id

The grant uses the Clone Service entitlement repository directly. There is no direct entitlement-write HTTP route in the monolith. For paid creation, Economy verifies the deposit first, creates the clone shell and default draft, writes the purchase row, then grants the entitlement in one backend flow.

Storage

One table for v1, one row per deposit:

sql
economy_purchases
  id                       TEXT PK             -- uuid v4
  user_id                  TEXT NOT NULL FK    -- users.id
  clone_id                 INTEGER             -- nullable: filled when clone exists
  kind                     TEXT NOT NULL       -- 'audition_fee' (more values in future)
  tier                     TEXT NOT NULL       -- starter/builder/pro
  max_active_assets        INTEGER NOT NULL
  allowed_model_strength   TEXT NOT NULL       -- cheap/medium/best
  amount_wei               TEXT NOT NULL       -- expected fee
  on_chain_amount_wei      TEXT NOT NULL       -- amount returned by getDeposit
  currency                 TEXT NOT NULL       -- 'ETH'
  chain_id                 INTEGER NOT NULL
  vault_address            TEXT NOT NULL       -- contract instance
  resolver_address         TEXT NOT NULL
  deposit_nonce            TEXT NOT NULL       -- decimal string of uint256
  status                   TEXT NOT NULL       -- pending/confirmed/granted/failed
  granted_entitlement_clone_id INTEGER         -- redundant with clone_id, kept for audit
  failure_reason           TEXT
  created_at               INTEGER NOT NULL
  updated_at               INTEGER NOT NULL
  UNIQUE (resolver_address, deposit_nonce)     -- idempotency

The (resolver_address, deposit_nonce) unique index is the canonical idempotency key — frontend can safely retry with the same nonce.

Endpoints

text
POST /api/v1/economy/audition-fee/quote
  Auth: Bearer
  Request:  { tier, assetCapacity }
  Response: { tier, assetCapacity, modelStrength, amountWei, currency, vaultAddress, resolverAddress, chainId }

POST /api/v1/economy/audition-fee/submit
  Auth: Bearer
  Request:  { depositNonce, tier, assetCapacity, modelStrength, cloneId? | createClone? }
  Response: { purchase: { id, status, cloneId }, entitlement, clone?, draft? }
  Errors:   401 unauthorized, 404 clone_not_found, 403 forbidden,
            409 deposit_already_redeemed, 422 deposit_amount_mismatch,
            422 deposit_not_found

/quote exists so the frontend can show the user the exact amount and the on-chain target before prompting for the wallet transaction. /submit is the v1 post-deposit confirmation/sync step. It still accepts depositNonce because there is no server-side purchase intent or SignedVault watcher yet.

Configuration

VariableDescriptionRequired
MEGAETH_VAULT_ADDRESSDeployed SignedVault address.Yes
MEGAETH_PUMP_PARTY_VAULT_RESOLVER_PRIVATE_KEYHex private key for the pump-party resolver. Must NOT also be the contract owner.Yes (for /submit + signing)
MEGAETH_RPC_URLRPC endpoint for read+verify. Falls back to viem default if unset.No
MEGAETH_VAULT_CHAIN_IDActive chain id (4326 mainnet, 6343 testnet). Defaults to testnet in dev.No
STRATEGY_BACKEND_SERVICE_TOKENExisting service token reused for cross-module entitlement sync.Yes

Production Checklist

  • Resolver private key lives in the production secret store, not .env.
  • MEGAETH_VAULT_ADDRESS is checksum-normalized; mismatch triggers refusal at startup.
  • Resolver address is NOT also the contract owner / upgrade admin.
  • Background job calls auth_challenges.deleteExpired daily (existing user-service concern, not economy).
  • (Future) Add a periodic reconciliation that walks economy_purchases.status='pending' and re-checks chain state.

Economy V2 / Out of Scope For V1

  • Race tickets (deposit verification reuses the same chain helper, but the schema/route lives in lifecycle work).
  • Settlement / payouts / creator royalty / protocol rake.
  • ERC20 (USDC) payments via Permit2.
  • On-chain event listener / indexer — v1 is pull-on-submit only. The frontend tells the backend to verify after the wallet transaction confirms.

Open Decisions

  • Vault contract owner: multisig vs. separate admin key. Out of backend PR's scope but blocks production deploy.
  • Whether protocol rake/creator pool flow through the same SignedVault or a separate splitter contract. Not needed until lifecycle PR.