maurerit 9a4f05b03f
Build and Push Container Images / pr-gates (push) Has been skipped
Build and Push Container Images / push-publish-ui (push) Successful in 2m14s
Build and Push Container Images / pr-publish-backend (push) Has been skipped
Build and Push Container Images / pr-publish-ui (push) Has been skipped
Build and Push Container Images / push-publish-backend (push) Successful in 2m50s
Merge pull request 'fix: merge T2 assignment eligibility hotfix into develop' (#514) from hotfix/512-invention-eligibility into develop
Reviewed-on: #514
Reviewed-by: maurerit <maurer.it@gmail.com>
Reviewed-by: gippity <gippity@r3r4um.net>
Reviewed-by: gollum <7+gollum@noreply.git.r3r4um.online>
2026-09-21 04:09:10 +00:00
2026-07-15 22:11:48 -04:00
2026-07-15 06:49:19 -04:00
2026-09-03 12:24:08 -04:00
2026-05-16 21:42:18 -04:00

EVE Market Calculator

Real-time market analysis and manufacturing planner for EVE Online — discovers profitable build opportunities across regions, plans recursive bills-of-materials down to raw minerals, and coordinates work assignments across a team of characters.

Features

  • Market opportunity discovery — Fetches live ESI market orders for all known regions, computes build costs, and surfaces items where manufacturing profit exceeds a configurable margin threshold
  • Full manufacturing cost model — Materials cost, job install fee (EIV × system cost index × facility tax + SCC surcharge + alpha clone tax), and optional opportunity cost
  • Invention cost modeling — T2 items include amortized invention cost (datacores, T1 BPC, install fee, failure probability, optional decryptor). Invented items show a badge in the UI with per-run cost breakdown
  • Recursive BOM expansion — Walks the full manufacturing tree (T2 → T1 components → reactions → raw minerals/moon goo), factoring invention and decryptors. Identifies bottleneck nodes by ISK contribution and surfaces multi-tier recursive opportunities
  • On-demand build calculator — Request a one-off build cost calculation with overrides: custom facility, runs, decryptor choice for invention
  • 2-phase assignment flow — Corp members are managed as named People; opportunities are assigned to them from a cart workflow with drag-and-drop support
  • Assignment task types — Manufacture, react, invent, buy, haul. Expand buildable assignments into their sub-component tasks recursively
  • Invention task generation — Aggregates T2 assignments and creates invention tasks with success probability + configurable overage, structured per-candidate results, and idempotent lifecycle preservation
  • Purchase lists — Aggregates raw material quantities needed across all active assignments, grouped by region. JSON or TSV export
  • Share assignments — Grant other EVE characters admin or member-level access to view/edit your assignment lists
  • Assigned to Me view — Characters see their own assignments with assigner name and portrait resolved via ESI
  • CSV export — Assignments exportable as CSV for external tooling
  • Build blocklist — Exclude unwanted items from the calculation pipeline
  • Market group filtering — Opportunities filterable by SDE market group
  • Build cost history — Time-bucketed build cost and margin trends per item/region with pruned-list transition tracking
  • Dashboard — Live stats: opportunity count (today/yesterday), average margin, top region, user assignment count, top 10 opportunities by margin

Architecture

  • Market Fetcher — CronJob: fetches ESI market orders for all known regions, refreshes stale ESI industry data (adjusted prices, system cost indices, facilities), stores in TimescaleDB, signals API server when done
    • eve-calc-fetcher — every 5 hours (0 */5 * * *)
  • API Server — Go HTTP service: queues compute tasks via RabbitMQ, serves opportunities, assignments, people, cart, purchase lists, shares, recursive BOM, and on-demand build requests
  • Compute Workers — 3 replicas consuming from RabbitMQ: calculates build costs through the recursive BOM engine, fetches adjusted prices and system cost indices, stores recursive snapshots plus legacy-compatible build_costs rows
  • Pare-Down Job — CronJob: filters build_costs to ≥ PROFIT_THRESHOLD_MARGIN (default 10%), populates pruned_list
    • eve-calc-paredown — every 5 hours at :30 (30 */5 * * *)
  • SDE Import Job — One-shot Job: downloads CCP's latest Static Data Export YAML zip and reloads all sde_* tables (types, groups, categories, blueprints, materials, regions, constellations, solar systems, stations)
  • SSO Backend — Node.js (Koa) EVE SSO OAuth2 proxy; handles login/callback/logout, fetches character + corp info from ESI, signs a session cookie, and reverse-proxies /api/* to the Go API server while injecting X-Member-ID
  • Frontend — React 19 + TypeScript + Vite 6 + MUI 7 + ECharts + dnd-kit SPA

Dependencies

  • TimescaleDB (PostgreSQL with TimescaleDB extension)
  • RabbitMQ
  • EVE ESI API (public endpoints + authenticated character endpoints via SSO backend)

ESI Scopes

The SSO backend requests scopes per flow. Public data (market orders, type names, etc.) uses no authentication.

Flow Scopes requested Purpose
Login esi-search.search_structures.v1 Authenticate character identity
Industry tracking esi-industry.read_corporation_jobs.v1 Sync corporation industry jobs
Asset tracking esi-assets.read_corporation_assets.v1 esi-markets.read_corporation_orders.v1 Sync in-hangar assets and open sell orders for corp stock
Wallet tracking esi-wallet.read_corporation_wallets.v1 esi-corporations.read_divisions.v1 Sync corporation wallet divisions and ledger data
Skills esi-skills.read_skills.v1 Validate assignment eligibility
Integration Classification Authorization ownership
Assets and corporation market orders Corporation-wide One authorizer per corporation
Corporation wallets and divisions Corporation-wide One authorizer per corporation
Corporation industry jobs Corporation-wide One authorizer per corporation
Character skills Character-owned Per-character tokens

Future scoped integrations must choose a classification before implementation.

Development Formatting

The repository-level development tools require Node.js 20.17 or newer. Install them after cloning:

npm install

The install configures a Husky pre-commit hook. It runs Prettier only for staged TypeScript files under eve-market-calc-ui/frontend/src/ and eve-market-calc-ui/sso-backend/src/, then re-stages the formatted results.

Format or check both complete source trees manually with:

npm run format
npm run format:check

Quick Start

# Run migrations
make migrate PG_DSN="postgres://user:pass@host:5432/eve_calc?sslmode=disable"

# Build all 9 binaries (api-server, market-fetcher, compute-worker, pare-down, sde-import, job-sync, asset-sync, skill-sync, wallet-sync)
make build

# Build Docker images
make docker-build

# Push to registry
make docker-push

# Deploy to K8s
./k8s/rollout.sh dev --dry-run  # dev lane (:latest-pr, namespace eve-calc-dev)
./k8s/rollout.sh dev
./k8s/rollout.sh uat --dry-run  # UAT lane (:latest, namespace eve-calc)
./k8s/rollout.sh uat
# Deployment docs: docs/deployment/gitflow.md, environments.md, runbook.md

# Manually trigger CronJobs
./scripts/trigger-jobs.sh uat           # UAT fetcher + paredown + jobsync
./scripts/trigger-jobs.sh dev fetcher   # dev fetcher only
./scripts/trigger-jobs.sh uat paredown  # UAT paredown only

API Endpoints

All endpoints prefixed at the API server's API_LISTEN_ADDR (default :8080).

X-Member-ID is the authenticated EVE character ID, injected by the SSO backend reverse proxy.


System

Method Path Auth Description
GET /health — DB connection health check; returns {"status":"ok"} or 503
POST /fetch_finished — Internal webhook from market fetcher; queues compute tasks for buildable items in the region

Asset Tracking And Stockpile

Asset sync publishes one complete corporation snapshot each hour. Published snapshots, their locations, stacks, and diagnostics are retained for 14 days. Physical Stockpile totals use every row of the current complete ESI snapshot, including unresolved evidence. Configured offices and hangars are future operational metadata and do not change current totals. Open sell-order escrow is deliberately excluded from physical totals, even though the asset authorization also requests the market-orders scope for corporation market-stock features.

Player-owned structures that cannot be resolved are recorded as unresolved_player_structure diagnostics and remain included in corporation-wide totals. Resolving those structures is outside the current #376 boundary; the diagnostics are the source of truth until that work is available.

The integration owner is the character that authorized the corporation token. Only that owner can configure observed offices, select their observed hangars, and grant or revoke Asset Managers. Asset Managers can view the Stockpile and the configured office list, but cannot see grant metadata, candidates, or mutation controls. Ordinary members cannot access Stockpile administration. The server-provided access booleans are the source of truth for every UI and API authorization decision. Aggregate builder availability may use corporation stock, but it does not reveal individual asset stacks, office choices, token diagnostics, or manager grants.

Method Path Access Description
GET /asset-tracking/status authenticated member Integration status and server authorization booleans
POST /asset-tracking/disable integration owner Disable the corporation asset integration
POST /asset-tracking/reset-errors integration owner Reset integration error state
GET /asset-tracking/snapshot/health Asset Manager Current snapshot health
GET /asset-tracking/snapshots Asset Manager Retained snapshot list
GET /asset-tracking/snapshot-failures Asset Manager Retained snapshot failure list
GET /asset-tracking/snapshots/{snapshot_id}/locations Asset Manager Locations for one snapshot
GET /asset-tracking/snapshots/{snapshot_id}/stacks Asset Manager Filterable physical stacks for one snapshot
GET /asset-tracking/snapshots/{snapshot_id}/diagnostics Asset Manager Resolution diagnostics for one snapshot
GET /asset-tracking/offices Asset Manager Configured office list; non-owners receive read-only state
GET /asset-tracking/office-options Asset Manager Observed location and hangar choices
POST /asset-tracking/offices integration owner Add an observed office and hangars
PATCH /asset-tracking/offices/{office_id} integration owner Update an observed office and hangars
DELETE /asset-tracking/offices/{office_id} integration owner Remove an observed office
GET /asset-tracking/managers integration owner Manager grant metadata
GET /asset-tracking/manager-candidates integration owner Known same-corporation grant candidates
POST /asset-tracking/managers integration owner Grant an Asset Manager
DELETE /asset-tracking/managers/{character_id} integration owner Revoke an Asset Manager

Asset snapshot migration and integration tests require TEST_PG_DSN pointing to a disposable PostgreSQL/ TimescaleDB database. Do not treat a skipped database test as migration verification.


Opportunities

Method Path Auth Description
GET /opportunities — Paginated profitable items from today's pruned list
GET /groups — SDE market groups with opportunity counts
GET /recursive-bom/opportunities — Multi-tier opportunities where building recursively beats buying the finished good

GET /opportunities query params

Param Default Description
region (all) Filter by region name (e.g. The Forge)
min_margin 0.20 Minimum margin (decimal, e.g. 0.10 = 10%)
sort_by profit One of: profit, margin, sell_price, build_cost
limit 50 Page size (max 50)
offset 0 Pagination offset
group_id (all) Filter by SDE market group ID
exclude_assigned_for_person_id (none) Mark items already assigned to this person

Opportunity response shape

{
  "opportunities": [
    {
      "item_id": 2456,
      "item_name": "Hobgoblin II",
      "group_id": 100,
      "group_name": "Combat Drone",
      "region": "The Forge",
      "build_cost": 293183.54,
      "sell_price": 310700.0,
      "profit": 17516.46,
      "margin": 0.0564,
      "buildable_at": "2026-04-26T00:02:13Z",
      "assigned": false,
      "materials_cost_isk": 223110.0,
      "manufacturing_job_cost_isk": 22301.89,
      "manufacturing_time_cost_isk": 0.0,
      "adjusted_manufacturing_time_seconds": 1224,
      "system_cost_index": 0.1725,
      "is_invented": true,
      "invention_cost_per_t2_run": 47771.65
    }
  ],
  "total": 1331
}

Dashboard & Analytics

Method Path Auth Description
GET /dashboard optional X-Member-ID Aggregated stats and top 10 opportunities. Header used for user_assignment_count when present
GET /market/{item_id}/{region} — Time-bucketed buy/sell price history
GET /market/regions — Distinct region names for which market data exists
GET /history/{item_id}/{region} — Time-bucketed build cost history + pruned-list transitions
GET /items/search?q=&limit= — Item name autocomplete against SDE (min 2 chars)
GET /items/{item_id}/is-buildable — Whether an item has a manufacturing blueprint

Dashboard response shape

{
  "opportunity_count": 1331,
  "opportunity_count_yesterday": 1199,
  "avg_margin": 0.312,
  "top_region": "The Forge",
  "top_region_count": 667,
  "user_assignment_count": 5,
  "top_opportunities": [ { "item_id": 2456, "item_name": "Hobgoblin II", "..." : "..." } ]
}

Market history params

Param Default Options
from 24h ago ISO 8601
to now ISO 8601
interval 5m 5m, 1h, 1d

Build cost history params

Param Default Options
from 7d ago ISO 8601
to now ISO 8601
interval 1h 1h, 1d

Regions, Systems & Facilities

Method Path Auth Description
GET /regions — List all SDE regions
GET /regions/{region_id}/systems — Solar systems in a region
GET /systems/search?q=&limit= — Solar system autocomplete (min 2 chars); returns id, name, region, security
GET /facilities — List industry facilities; optional filters: region_id, system_id, facility_id

Builds & Invention Decryptors

Method Path Auth Description
POST /builds/request X-Member-ID Request an on-demand build calc with overrides (facility, runs, decryptor); returns 202
GET /builds/{request_id} — Retrieve a completed build calc by request id
GET /invention/decryptors — List all invention decryptors with probability/runs/ME/TE modifiers

POST /builds/request body:

{
  "request_id": "uuid-from-client",
  "item_id": 2456,
  "region": "The Forge",
  "invention": { "decryptor_type_id": 34204 },
  "mfg": { "facility_id": 60003760, "runs": 10 }
}

Recursive BOM

Method Path Auth Description
GET /recursive-bom/{type_id}?region_id=N — Cached recursive BOM snapshot for an item+region
POST /recursive-bom/{type_id}/refresh — On-demand recursive expansion; persists result
GET /recursive-bom/{type_id}/bottlenecks?region_id=N&limit=N — Top-N costliest sub-components

Assignments

Method Path Auth Description
POST /assign — Create a single assignment (legacy)
GET /assignments/{member_id} — Assignments for a corp member (legacy)
DELETE /assign/{member_id}/{item_id}/{region} — Delete by tuple (legacy)
POST /assignments/batch X-Member-ID Batch-assign cart items to people; clears cart
POST /assignments/plan/preview X-Member-ID Preview eligibility, queue, and compensation before creating assignments
POST /assignments/plan/confirm X-Member-ID Revalidate a reviewed plan and create its assignments
DELETE /assignments X-Member-ID Delete assignments for the authenticated user; optional ?filter=held|released|all (default all) scopes by release state
PATCH /assignments/{assignment_id} X-Member-ID Update runs, task_type, or region
PATCH /assignments/{assignment_id}/complete X-Member-ID Complete an assignment as a handoff: transactionally create a sell obligation receipt for eligible manufacture/react work, then delete the assignment row. Returns the completion result (deleted, obligation_created, obligation_id). 409 when eligible work has no build-cost snapshot to price the receipt
DELETE /assignments/{assignment_id} X-Member-ID Delete a single assignment
POST /assignments/expand X-Member-ID Expand buildable assignments into sub-component assignments (dry_run, recursive, and optional exclusion fields; see below)
POST /assignments/from-build X-Member-ID Create an assignment directly from a completed build calc
POST /assignments/invention-tasks X-Member-ID Generate aggregated invention tasks with success probability + overage
GET /assignments/assigned-to-me X-Member-ID Assignments targeting the authenticated character, with assigner name + portrait
GET /assignments/export X-Member-ID Download assignments as CSV

POST /assignments/expand exclusions

The optional exclude_fuel_blocks, exclude_t1_hulls, and exclude_t1_tools boolean fields independently control expansion filtering. An explicitly supplied true or false is authoritative; an omitted field falls back to its corresponding EXPAND_EXCLUDE_* environment default (or false when that variable is absent).

Assignment eligibility and invention tasks

An invented-product manufacture assignment is eligible only when the assignee meets both its manufacturing requirements and the configured invention floors (INV_ENCRYPTION_SKILL, INV_DATACORE1_SKILL, and INV_DATACORE2_SKILL). These configured floors remain authoritative for assignment eligibility and invention modeling. Non-invented manufacturing, reaction, and explicit invention assignments retain their respective existing requirements.

Existing manufacturing assignments are accepted commitments. POST /assignments/invention-tasks does not re-check their current invention eligibility; it accounts for every (person_id, item_id, region) candidate as created, updated, or a structured skip. Regeneration may refresh mutable task inputs, but preserves existing assigned_at and released_at timestamps.

POST /assignments/batch request

{
  "assignments": [
    { "item_id": 2456, "region": "The Forge", "person_id": 7 }
  ]
}

Assignment planning

POST /assignments/plan/preview accepts one or more assignment intents. A preview may omit person_id to compare eligible people. Confirmations send all intents for fingerprint validation, but require at least one staged intent with a non-null person_id; only staged intents are inserted as assignments and removed from the cart, so unstaged cart items remain. Monetary values, percentages, and ISK-per-slot-hour values are decimal strings so clients must not parse them as floating-point values. Nullable preview fields mean the corresponding market, blueprint, queue, or policy input is unavailable.

workspace_owner_id is optional: omitted, null, or blank selects the caller's workspace. An explicit owner is allowed only to that owner or an active admin share recipient; member shares cannot plan or confirm work. Confirmations return 409 with code: "duplicate_confirmation" when the same manual work was already confirmed.

{
  "workspace_owner_id": "owner-id",
  "assignments": [
    { "item_id": 2456, "region": "The Forge", "person_id": null, "runs": 10 }
  ]
}

The preview response includes items, totals, fingerprint, generated_at, requires_acknowledgement, and diagnostics. To create assignments, submit the same intents with selected people and the returned fingerprint to POST /assignments/plan/confirm. Set assign_without_projection only after explicitly acknowledging a preview that reports requires_acknowledgement: true; release controls whether created assignments are immediately released.

Representative successful 200 preview response:

{
  "items": [
    {
      "intent": { "item_id": 2456, "region": "The Forge", "person_id": null, "runs": 10 },
      "item_name": "Hobgoblin I",
      "current_margin": "18.250000",
      "trend": "flat",
      "output_quantity_per_run": 1,
      "adjusted_seconds_per_run": null,
      "market_volume_30d": 1250.5,
      "market_volume_as_of": "2026-08-30",
      "build_snapshot_at": "2026-08-30T12:00:00Z",
      "equivalent_market_volume_days": "0.01",
      "economics": {
        "projected_output_quantity": 10,
        "unit_sell_price": "12500.00",
        "unit_build_cost": "9800.00",
        "gross_revenue": "125000.00",
        "cost_basis": "98000.00",
        "gross_profit": "27000.00",
        "gross_profit_per_output_unit": "2700.00",
        "transaction_tax": "1250.00",
        "broker_fee": "1250.00",
        "distributable": "24500.00",
        "slot_seconds": null
      },
      "assignees": [
        {
          "person_id": 7,
          "character_name": "Builder One",
          "character_portrait": "",
          "eligibility": "unknown",
          "active_assignment_count": 3,
          "queue_before_seconds": 43200,
          "proposed_added_seconds": null,
          "queue_after_seconds": null,
          "builder_percentage": "25.000000",
          "builder_cut": "6125.00",
          "isk_per_slot_hour": null,
          "policy_id": 12,
          "policy_version": 3,
          "policy_effective_at": "2026-08-01T00:00:00Z",
          "diagnostics": [
            { "code": "unknown_skills", "severity": "blocking", "message": "unknown_skills" }
          ]
        }
      ],
      "diagnostics": [
        { "code": "unknown_duration", "severity": "warning", "message": "unknown_duration" }
      ]
    }
  ],
  "totals": {
    "capital": null,
    "gross_sale_value": null,
    "gross_profit": null,
    "builder_cut": null,
    "output_quantity": null,
    "slot_seconds": null,
    "equivalent_market_volume_days": null
  },
  "fingerprint": "7b6c5f2e6db352513ef63e0a75da36b4cf749a8428c39535f0d4d1e924f81c3b",
  "generated_at": "2026-08-30T12:00:00Z",
  "requires_acknowledgement": false,
  "diagnostics": []
}
{
  "assignments": [
    { "item_id": 2456, "region": "The Forge", "person_id": 7, "runs": 10 }
  ],
  "fingerprint": "preview-fingerprint",
  "assign_without_projection": true,
  "release": false
}

Successful confirmation returns 201 with created, released, and assignment_ids:

{
  "created": 1,
  "released": false,
  "assignment_ids": [4821]
}

Conflicts return typed JSON. If authoritative inputs have changed, confirmation returns 409 with a replacement preview to review and submit again:

{
  "code": "projection_changed",
  "message": "Assignment inputs changed; review the refreshed projection.",
  "preview": {}
}

GET /assignments/assigned-to-me response shape

{
  "assignments": [
    {
      "assignment_id": 82,
      "item_id": 64034,
      "item_name": "Boobook",
      "region": "Sinq Laison",
      "assigned_at": "2026-04-26T02:28:09Z",
      "runs": 5,
      "task_type": "manufacture",
      "assigned_by": "93461829",
      "assigned_by_name": "Matthew Maurer",
      "assigned_by_portrait": "https://images.evetech.net/characters/93461829/portrait?size=64",
      "assigned_to_name": "Alt Character"
    }
  ]
}

Dividend Policies

Dividend policy configuration is authenticated with X-Member-ID. Every route resolves the caller's current corporation server-side; clients cannot select or override the corporation scope. Current corporation members may read policies and active administrator grants. The corporation wallet integration owner and active policy administrators may create, replace, preview, and publish drafts, while only the wallet integration owner may grant or revoke policy administrators.

Method Path Auth Description
GET /api/dividend/policies X-Member-ID List the current draft, when present, and published policy summaries newest first
GET /api/dividend/policies/{id} X-Member-ID Get a corporation policy version and its ordered buckets
POST /api/dividend/policies/draft X-Member-ID (policy editor) Return the existing draft or create one from the latest publication or minimal remainder-only template
PUT /api/dividend/policies/draft X-Member-ID (policy editor) Atomically replace the complete draft description and bucket list using expected_revision
POST /api/dividend/policies/draft/publish X-Member-ID (policy editor) Immediately publish the draft using expected_revision; the resulting version is immutable
POST /api/dividend/policies/preview X-Member-ID (policy editor) Validate and allocate an unsaved policy and amount without persisting either
GET /api/dividend/policy-admins X-Member-ID List active policy administrator grants for the caller's corporation
POST /api/dividend/policy-admins X-Member-ID (wallet owner) Grant policy administration to a current corporation member by character_id
DELETE /api/dividend/policy-admins/{characterID} X-Member-ID (wallet owner) Revoke an active policy administrator grant while retaining its history

Percentages such as bucket percentage values are canonical decimal strings with six fractional digits, for example "25.000000". Money fields such as preview distributable_amount and allocation amount are canonical decimal strings with two fractional digits, for example "1250.00"; clients must not send JSON numbers for these exact values.

Every policy payload and preview includes builder_distribution_mode, either "direct" or "shared_pool". A draft replacement includes it with the complete bucket list:

{
  "expected_revision": 4,
  "builder_distribution_mode": "shared_pool",
  "buckets": []
}

direct creates the existing builder-recipient liabilities. Under shared_pool, every positive role:builder allocation becomes an immutable corporation-wide builder-pool source instead. The mode is copied into drafts, previewed but not persisted by policy preview, and frozen when published; changing it never changes earlier liabilities or pool sources.

Draft replacement and publication requests include the draft's current expected_revision. A stale revision returns 409 Conflict and leaves the newer draft unchanged. Publication takes effect immediately: effective_at equals published_at, with no scheduling or backdating. Preview operates only on the supplied unsaved configuration and never creates or updates a policy, allocation event, balance, or liability.

This API is configuration-only. Profit derivation, persisted allocation events, payout liabilities and batches, reserve or budget balances, surplus distribution, and wallet transfers remain outside this issue's scope.


Dividend Payouts

Dividend payout routes require X-Member-ID and resolve the actor and corporation server-side. The current wallet authorizer may grant or revoke payout administrators and has payout-administrator access. Active payout-administrator delegates may create batches and resolve ambiguous matches, but cannot grant or revoke authority. Recipients can read only their own liabilities, batches, transfer evidence, and carry-forward; payout administrators can read corporation-wide payout records.

Method Path Auth Description
GET /api/dividend/payout/status X-Member-ID Return the caller's corporation, payout authority, and match-window metadata
GET /api/dividend/payout-admins X-Member-ID List active payout administrators
POST /api/dividend/payout-admins X-Member-ID (wallet authorizer) Grant payout administration to a current corporation character by character_id
DELETE /api/dividend/payout-admins/{characterID} X-Member-ID (wallet authorizer) Revoke an active payout-administrator grant
GET /api/dividend/payout/liabilities X-Member-ID (recipient or payout admin) List server-scoped liabilities with derived payout state
POST /api/dividend/payout/batches X-Member-ID (payout admin) Claim selected liabilities and create recipient transfers for a wallet division
GET /api/dividend/payout/batches X-Member-ID (recipient or payout admin) List server-scoped batches and aggregate states
GET /api/dividend/payout/batches/{batchID} X-Member-ID (recipient or payout admin) Return scoped lines, transfers, carry applications, and settlement evidence
GET /api/dividend/payout/transfers/{transferID}/candidates X-Member-ID (payout admin) List current unconsumed qualifying candidates for an ambiguous transfer
POST /api/dividend/payout/transfers/{transferID}/match X-Member-ID (payout admin) Select and immutably consume one qualifying journal entry

All money values are canonical decimal JSON strings, not JSON numbers. Payouts are settled from retained wallet evidence; there is no manual mark-paid action. A qualifying entry is an unconsumed journal row for the batch corporation and wallet division with ref_type corporation_account_withdrawal, a negative amount, and the recipient in second_party_id. Its date must be within the batch's stored inclusive match window and its absolute amount must be within the inclusive 10 percent tolerance of the expected transfer. Wallet sync automatically settles exactly one qualifying entry after successful journal pagination; multiple candidates remain ambiguous for an administrator to select, and expired unresolved transfers remain stale with their liabilities claimed.

Shared builder pool

Shared-pool routes use the caller's current corporation derived from X-Member-ID and require payout-administrator authority.

Method Path Access Description
GET /api/dividend/payout/builder-pool payout admin Return all currently unclaimed pool sources, their exact total, count, oldest accrual time, and source_set_token
GET /api/dividend/payout/builder-pool/characters?q=&limit=&offset= payout admin Search eligible app-linked recipient characters; limit defaults to 50 and must be 1 through 50, and offset defaults to 0
GET /api/dividend/payout/builder-pool/people payout admin List the caller's People entries with shared-pool eligibility and available direct-liability totals
POST /api/dividend/payout/builder-pool/preview payout admin Review a complete-pool distribution without writing state
POST /api/dividend/payout/builder-pool/distributions payout admin Atomically claim and distribute the reviewed complete pool, creating a regular payout batch

An eligible recipient is any character with exactly one current user_characters owner link. The search does not require corporation membership, builder role, assignment or payout history, or policy participation. Both preview and distribution require wallet_division from 1 through 7, a non-empty source_set_token, and one or more unique positive recipient_character_ids; distribution also requires the non-empty preview_token returned by preview.

The pool is the full set of unclaimed positive shared-pool builder sources across historical policy versions, even when the current policy is direct. Preview sorts recipients by character ID and divides the entire pool equally in cents, assigning remainder cents in that order. It also includes every currently available non-pool liability for each selected character and applies that character's carry to calculate the expected transfer. Distribution rechecks the source set, selected recipients' ownership, automatically included liabilities, and carry before committing the source claims, one generated pool liability per recipient, and the consolidated payout batch together. A changed source set or reviewed fact returns 409 Conflict with {"code":"builder_pool_stale"}; clients must reload the summary and preview rather than retrying stale tokens.


Dividend Earnings

Dividend earnings routes require X-Member-ID and always resolve the actor and current corporation server-side. Recipients receive only records owned by their authenticated member identity. A recipient_character_id selector is forwarded to the database's authoritative payout-administrator check; it is not authorized by route parsing. Payout administrators may use that selector to inspect one recipient's records, while ordinary recipients cannot use it.

Method Path Auth Description
GET /api/dividend/earnings/obligations X-Member-ID (recipient or payout admin) List recipient-scoped sale obligations and aggregate earnings
GET /api/dividend/earnings/obligations/{obligationID} X-Member-ID (recipient or payout admin) Return one scoped obligation with chronological sale, allocation, liability, settlement, and diagnostic evidence
GET /api/dividend/earnings/paychecks X-Member-ID (recipient or payout admin) List recipient-scoped payout transfers newest first
GET /api/dividend/earnings/paychecks/{transferID} X-Member-ID (recipient or payout admin) Return one scoped transfer grouped by obligation and fixed sources, with carry-forward evidence

Obligation lists accept view=active|history (active by default), from, to, limit, cursor, and optional recipient_character_id. Paycheck lists accept from, to, limit, cursor, and optional recipient_character_id; either detail route accepts optional recipient_character_id. Dates use YYYY-MM-DD in America/New_York: from is inclusive at New York midnight and to is exclusive at the following New York midnight. limit defaults to 50 and must be 1 through 100; cursor is opaque and must be returned unchanged.

An obligation is open until a recipient allocation settles, partially_paid after some allocation settles, and paid only when it is fulfilled, fully posted, and every recipient liability settles. Paychecks represent grouped payout transfers rather than new earnings: their expected amount includes the selected liability total less any carry application, and settlement, ambiguity, and stale status are based on retained wallet evidence. All monetary values are decimal JSON strings, never JSON numbers.


Sell Obligations

A sell obligation is a durable receipt created when an eligible final-product assignment (manual manufacture/react) is completed — see PATCH /assignments/{assignment_id}/complete. It records produced quantity (runs × SDE output quantity per run), the per-unit and total build cost / projected sell price locked at completion, and split builder/owner/assigner/completer provenance. assignment_id is provenance only (no FK); receipt creation is idempotent, so duplicate/concurrent completions never create duplicate receipts.

Method Path Auth Description
GET /api/sell-obligations?status=outstanding|fulfilled&limit=50&cursor=... X-Member-ID List obligations visible to the caller (owner + existing share model). Also supports open, partially_filled, all, and filters for corporation_id, item_id, region, search, wallet_division, fulfilled_from, and fulfilled_to
GET /api/sell-obligations/filter-options?status=fulfilled X-Member-ID List visibility-scoped corporation, region, and wallet-division options for fulfilled history
GET /api/sell-obligations/{id} X-Member-ID Single obligation if visible to the caller, else 404
GET /api/sell-obligations/{id}/sales X-Member-ID Immutable sale receipts for a visible obligation, ordered oldest sale first; an invisible or unknown obligation returns 404

outstanding combines open and partially filled obligations. Active results are ordered by created_at DESC, id DESC; fulfilled history is ordered by fulfilled_at DESC, id DESC. Paginated responses may include an opaque, status-scoped next_cursor; pass it back unchanged with the same status and limit to request the next page.

Fulfilled date filters use UTC calendar dates: fulfilled_from is inclusive at 00:00 UTC, while fulfilled_to includes the whole specified date by using the following midnight as an exclusive bound. wallet_division selects obligations having at least one receipt from that division, but obligation totals continue to include every receipt across all divisions.


Dividend Allocation Posting

After wallet sale reconciliation commits, wallet sync posts each retained sale receipt independently to one immutable dividend allocation event. Posting does not wait for the parent obligation to be fulfilled. It snapshots gross revenue, receipt quantity, locked unit build cost, receipt cost basis, taxes, broker fee, policy, allocation lines, and recipient identity; later receipts or configuration changes do not rewrite an existing event.

The applicable policy is the latest published version whose effective_at is at or before the receipt's sold_at, not the time wallet sync posts it. Positive resolved person and role allocations become payout liabilities. Reserve, budget-account, and remainder allocations become internal balance events instead; they are not payout liabilities. A negative distributable amount becomes a signed remainder loss event and never a negative human liability.

Receipts that cannot post remain retained and are retried in a later wallet-sync pass. dividend_allocation_diagnostics records the current retry state (policy_not_found, person_recipient_unresolved, source_data_invalid, or posting_failed), attempt count, and timestamps. A successful post removes its active diagnostic. An unresolved role is instead recorded on its immutable allocation line as role_recipient_unresolved and its amount is routed to remainder.

The fee-rate settings below are temporary estimated fallbacks pending actual transaction-tax correlation (#439) and broker-fee attribution (#440). Posted events snapshot both the configured rate and estimated provenance, so later actual-fee work applies only to future receipts.

For existing corporations where receipts predate policy version 1, the optional one-off script previews the timestamp shift without changing data:

PG_DSN="postgres://user:pass@host:5432/eve_calc?sslmode=disable" \
  ./scripts/backdate-dividend-policy-timeline.sh

After reviewing the preview, apply the same per-corporation shift explicitly:

PG_DSN="postgres://user:pass@host:5432/eve_calc?sslmode=disable" \
  ./scripts/backdate-dividend-policy-timeline.sh --apply

The script shifts every published version's published_at and effective_at by the same offset so version 1 aligns with the oldest receipt while policy ordering and intervals remain unchanged. It is never run by migrations, application startup, wallet sync, or Kubernetes workloads.


People

Method Path Auth Description
POST /people X-Member-ID Add an EVE character to the user's people list
GET /people X-Member-ID List all people for the authenticated user (with portrait URL and is_reactor)
PATCH /people/{person_id} X-Member-ID Toggle the is_reactor role flag (caller must own the person)
DELETE /people/{person_id} X-Member-ID Remove a person
GET /people/{person_id}/assignments X-Member-ID Assignments for a person with runs and computed manufacturing hours

POST /people request

{ "character_id": 93461829, "character_name": "Alt Character" }

PATCH /people/{person_id} request

{ "is_reactor": true }

Flagging a person as a reactor opts their corporation into the shared reaction pool (issue #235): Fill Buildables routes reaction materials to the corp's single implicit pool (assignments.pool_corp_id, person_id NULL) instead of inheriting the parent person, and the reactor's Assigned to Me (/assignments/assigned-to-me/job-progress) gains the pool rows with progress aggregated across all corp members' reaction jobs.


Cart

Method Path Auth Description
POST /cart X-Member-ID Add a single item to cart
POST /cart/batch X-Member-ID Add multiple items to cart
GET /cart X-Member-ID Cart contents enriched with item names and facility names
GET /cart/count X-Member-ID Cart item count
DELETE /cart/{item_id}/{region} X-Member-ID Remove a single item from cart
DELETE /cart X-Member-ID Clear cart

Purchase List

Method Path Auth Description
GET /purchase-list X-Member-ID Aggregated raw materials needed for all active assignments, grouped by region (JSON). Each item carries a routed source (market/corp) per the routing rules below
GET /purchase-list/tsv X-Member-ID Full purchase list (both sources) as a TSV download (purchase-list.tsv)
GET /purchase-list/market/tsv X-Member-ID Market-routed items only, EVE multibuy-friendly TSV (purchase-list-market.tsv)
GET /purchase-list/corp-mail X-Member-ID Corp-routed items as readable plain text for an EVE mail / corp chat (purchase-list-corp.txt)

All four endpoints accept optional boolean query parameters exclude_fuel_blocks, exclude_t1_hulls, and exclude_t1_tools. Matching manufactured intermediates become purchase leaves and their descendants are omitted. Reaction children remain exempt from these exclusions, and normal purchase routing still applies to the leaves.

For manufacture, react, and invent assignments, demand uses assigned runs not yet represented by correlated industry jobs in active, ready, or delivered status. Installed job runs are counted in full and clamped at zero; buy and haul behavior is unchanged.


Purchase Routing (issue #231)

Configurable rules that split remaining purchase demand into a Market path (buy in a trade hub / multibuy) and a Corp path (request from corp mates). Routing runs after corp asset-stock subtraction, so only the leftover to_buy quantity is routed. The effective source is resolved by precedence: type rule > group rule > category rule > user default rule > code-backed SDE seed default > Market fallback. Rules are user-scoped; a rule marked shared is reusable by people granted access via user_shares (sharing rules does not expand which assignments appear in a purchase list).

Method Path Auth Description
GET /purchase-routing/rules X-Member-ID Effective rules (own + shared-in, with resolved scope names, disabled flag, and the sharer's owner_name/owner_portrait), the seed defaults, and the Market fallback
POST /purchase-routing/rules X-Member-ID Create/upsert a rule {scope_kind, scope_id, source, shared} (scope_kind: type/group/category/default; scope_id omitted for default)
PATCH /purchase-routing/rules/{id} X-Member-ID Update an owned rule's {source, shared}
DELETE /purchase-routing/rules/{id} X-Member-ID Delete an owned rule
PUT /purchase-routing/rules/{id}/enabled X-Member-ID Recipient-side enable/disable of a shared-in rule {enabled}. Disabling mutes it for the caller only (it no longer affects their routing); the owner's rule is untouched
GET /purchase-routing/groups?q= X-Member-ID SDE group search for the rule picker
GET /purchase-routing/categories X-Member-ID Full SDE category list for the rule picker

Blocklist

Method Path Auth Description
GET /blocklist — Paginated blocklist entries
POST /blocklist X-Member-ID Add an item: type_id, reason
DELETE /blocklist/{type_id} X-Member-ID Remove an item from the blocklist

Shares (Cross-User Access)

Method Path Auth Description
POST /users/{target_user_id}/shares X-Member-ID (owner) Grant access to another character: grantee_character_id, role (admin/member)
GET /users/{target_user_id}/shares X-Member-ID (owner) List shares granted by this user
PATCH /users/{target_user_id}/shares/{grantee_user_id} X-Member-ID (owner) Update a share's role
DELETE /users/{target_user_id}/shares/{grantee_user_id} X-Member-ID (owner) Revoke a share
GET /users/my-shares X-Member-ID List shares granted to the caller
GET /users/{target_user_id}/people X-Member-ID (gated) View another user's people list (requires share)
GET /users/{target_user_id}/assignments X-Member-ID (gated) View another user's assignments grouped by person
GET /me/lists-i-can-edit X-Member-ID People+IDs where caller has admin share
GET /me/lists-i-can-view X-Member-ID People+IDs where caller has member or admin share

Frontend Routes

Path Description
/ Dashboard — stats, margins, top opportunities
/opportunities Browse profitable build opportunities with filters
/assignments Assignment management: cart, people, AssignedToMe, MyAssignments, Shares, Blocklist, PurchaseList tabs
/market Item/region market price viewer with charts
/history Build-cost history charts
/calculator On-demand build cost calculator with recursive BOM engine, facility/decryptor/invention overrides, and assignment creation
/recursive-bom Redirects to /calculator (preserves query params)
/blocklist Manage items excluded from build calculations
/settings User preferences, region selection, theme
/callback EVE SSO redirect landing page

Configuration

All config is driven by environment variables. See k8s/base/configmap.yaml.

Key variables

Variable Default Description
PG_DSN postgres://localhost:5432/eve_calc?sslmode=disable TimescaleDB connection string
RMQ_URL amqp://guest:guest@localhost:5672/ RabbitMQ connection string
API_LISTEN_ADDR :8080 API server listen address
API_URL http://eve-calc-api:8080 Market fetcher → API server URL
PROFIT_THRESHOLD_MARGIN 0.10 Minimum margin for pare-down (10%)
HOME_SYSTEM 30000142 Solar system ID for manufacturing cost index lookup (Jita)
HOME_FACILITY_ID 60003760 Facility ID for facility tax lookup (Jita 4-4)
MFG_BLUEPRINT_TE 20 Blueprint time efficiency level assumed for T1 items
MFG_INDUSTRY_SKILL_LEVEL 5 Industry skill level
MFG_ADVANCED_INDUSTRY_SKILL_LEVEL 5 Advanced Industry skill level
MFG_FACILITY_TIME_MULTIPLIER 1.0 Facility time multiplier (structure rig bonus)
MFG_FACILITY_COST_MULTIPLIER 1.0 Facility cost multiplier (structure rig bonus)
MFG_OPPORTUNITY_ISK_PER_HOUR 0 App-level opportunity cost ISK/hr (not an EVE mechanic)
INV_ENCRYPTION_SKILL 4 Encryption methods skill level for invention
INV_DATACORE1_SKILL 4 Datacore skill 1 level
INV_DATACORE2_SKILL 4 Datacore skill 2 level
INV_SCC_SURCHARGE 0.015 SCC surcharge rate for invention install fee (1.5%)
INV_AMORTIZE_FAILED true Amortize failed invention attempts into cost
INV_SYSTEM_ID 30000142 Solar system for invention cost index lookup
INV_FACILITY_ID 60003760 Facility for invention facility tax lookup
INVENTION_TASK_OVERAGE 0.10 Overage applied when generating invention tasks (10%)
EXPAND_EXCLUDE_FUEL_BLOCKS false Exclude Fuel Blocks during assignment expansion; initial UI default and omitted-request fallback; browser/API values may override.
EXPAND_EXCLUDE_T1_HULLS false Exclude direct Tech I base inputs to Tech II recipes (ships, modules, drones, and equivalents) during assignment expansion; initial UI default and omitted-request fallback; browser/API values may override.
EXPAND_EXCLUDE_T1_TOOLS false Exclude R.A.M./R.Db tools during assignment expansion; initial UI default and omitted-request fallback; browser/API values may override.
ESI_TIMEOUT_SECONDS 10 Per-request ESI HTTP timeout
MARKET_FETCHER_TIMEOUT 240 Market fetcher max run time in seconds
DIVIDEND_PAYOUT_MATCH_WINDOW_SECONDS 1209600 Payout journal match window in seconds (14 days); missing, malformed, or nonpositive values use this default
DIVIDEND_ESTIMATED_TRANSACTION_TAX_RATE 0.033750 Temporary estimated transaction-tax fallback pending #439; canonical decimal fraction with exactly six fractional digits, from 0.000000 through 1.000000; invalid explicit values fail startup
DIVIDEND_ESTIMATED_BROKER_FEE_RATE 0.015000 Temporary estimated broker-fee fallback pending #440; canonical decimal fraction with exactly six fractional digits, from 0.000000 through 1.000000; invalid explicit values fail startup

Operators may set the expansion exclusion variables in deployment overlays to choose intentional defaults, but must not add them to k8s/base/configmap.yaml merely to record the false defaults; absent variables preserve those defaults.

Secrets (not in configmap): k8s/overlays/uat/sealed-secrets.yaml / k8s/overlays/dev/sealed-secrets.yaml — encrypted via Sealed Secrets before committing.

Database Migrations

Migrations are embedded in the migrate binary (via //go:embed) and run automatically at deploy time via the eve-calc-migrate Job. Powered by golang-migrate.

# Description
001 Initial schema: market_orders, build_costs hypertables, pruned_list, assignments, 30d retention policies
002 SDE tables: types, groups, categories, blueprints, blueprint_materials
003 order_id promoted to BIGINT (EVE order IDs exceed INT4)
004 2-phase select: people, cart tables; person_id on assignments
005 assignments: +runs, +task_type, +assigned_by_user_id
006 sde_industry_activity_times table
007 build_costs: +manufacturing time columns
008 ESI cache tables (esi_industry_system_cost_indices, esi_industry_facilities, esi_market_prices); full build-cost breakdown columns
009 build_costs: +is_invented, +invention_cost_per_t2_run
010 esi_market_history daily-cache table
011 assignments: task_type NOT NULL with default manufacture (backfilled)
012 build_costs: +overrides_json, +requested_by
013 SDE universe: sde_regions, sde_constellations, sde_solar_systems, sde_stations
014 assignments: +facility_id, +decryptor_type_id, +source_request_id
015 build_costs: dedicated request_id column + unique partial index
016 build_blocklist table with auto-updated_at trigger
017 Allow coexisting Manufacture + Invention assignments for same (person, item, region)
018 Recursive BOM (spec 008): expanded_bom_nodes, build_cost_recursive; legacy watchlist table removed by migration 033
019 Fix casing bug: rename 'Manufacturing' task_type rows to 'manufacture'
020 user_shares table for cross-user assignment sharing (admin/member roles)
021 market_summary aggregate table; rebuilds market_orders with a (order_id, time) unique key + volume
022 compute_market_summary() SQL function (weighted avg / median / 5% tail stats per region snapshot)
023 Character cache: characters + user_characters tables (backfilled from people)
024 assignments: +released_at (held/released two-phase flow); backfilled to assigned_at
025 Industry-job tracking: corporation_tracking, character_scoped_tokens (encrypted ESI tokens) + update_updated_at_column() trigger
026 Assignment lifecycle: +completed_at, +status; industry_jobs +product_type_id
027 industry_jobs: +started_at (backfilled from created_at)
028 industry_jobs: rename runs_remaining→runs, drop percentage_complete, +duration/end_date/successful_runs
029 industry_job_errors log table (per corporation)
030 Drop legacy assignments (corp_member_id, item_id, region) unique constraint
031 industry_jobs: +activity_id (manufacture/invent/react), backfilled to 1
032 Corp asset tracking: corporation_assets + corporation_asset_config
033 Drop legacy recursive_bom_watchlist table
034 Drop build_blocklist.type_id FK (allow blocking non-SDE types)
035 Skill validation: character_skills + sde_blueprint_skills
036 purchase_routing_rules (type/group/category/default → market/corp source)
037 purchase_routing_rule_mutes (per-user rule muting)
038 Index sde_blueprints (product_type_id, activity)
039 Shared per-corp reaction pool: people +is_reactor, assignments +pool_corp_id + target/dedup constraints
040 assignments: +assignment_origin (manual/system_expand_buildables/system_invention_tasks)
041 character_scoped_tokens +has_orders_scope; corporation_market_stock table
042 warm_runs table (one-per-day compute warm-up guard)
043 sell_obligations receipt table: no FK to assignments; unique partial index on assignment_id for idempotent receipt creation
044 SDE market-group hierarchy and closure tables; sde_types.market_group_id
045 Retained corporation-wallet divisions, transactions, journal, sync progress/diagnostics, and sell-obligation sale allocations
046 Corporation integration ownership registry, deterministic asset/wallet backfill, duplicate cleanup, and token uniqueness
047 Corporation industry ownership backfill, corporation-token duplicate cleanup, and per-corporation token uniqueness
048 Corporation-scoped, versioned Dividend policies and buckets; retained policy-administrator grant history; published-policy immutability
049 Dividend payout ledger: payout administrators, liabilities, batches, transfers, and settlement evidence
050 Immutable receipt-level dividend sale-profit allocation events, lines, internal balance events, and retry diagnostics
051 Retained hourly asset snapshots, Stockpile access grants, observed offices, and asset-resolution diagnostics
052 Permit durable SDE metadata failure attempts
S
Description
No description provided
Readme
54 MiB
0 Stars 7 Watchers 0 Forks
2026-09-20 16:13:17 +00:00
Languages
Go 65.6%
TypeScript 31.7%
PLpgSQL 1.1%
Shell 0.9%
Python 0.5%
Other 0.1%