# EVE Online Profit Calculator & Task Planner Spec

**Version:** 0.1  
**Date:** 2026-04-09  
**Status:** Architecture & API Definition Phase

---

## 1. System Overview

The EVE Profit Calculator is a real-time market analysis system that identifies profitable manufacturing opportunities across EVE Online regions. It ingests market data, calculates build costs (including invention), filters by profitability, and assigns opportunities to corporation members.

### High-Level Data Flow

```
┌─────────────────────┐
│  Market Fetcher     │  Runs every 5 minutes
│  (K8s CronJob)      │  Queries ESI for Forge & Sinq Laison orders
└──────────┬──────────┘
           │ HTTP POST (region, items, prices)
           ▼
┌─────────────────────┐
│  TimescaleDB        │  Stores market_orders hypertable
│  (thehog)           │
└──────────┬──────────┘
           │
           │ Triggers via POST to API /fetch_finished
           ▼
┌─────────────────────────────────┐
│  API Server (Go)                │  Queries buildable items + pruned_list
│  - Accepts signal from fetcher  │  Bundles materials data → RabbitMQ
│  - CRUD corp assignments        │
│  - Queries opportunities        │
└──────────┬──────────────────────┘
           │
           │ Publishes messages to RabbitMQ (6 listeners, round-robin)
           ▼
┌─────────────────────────────────┐
│  Compute Workers (K8s)          │  2 per node × 3 nodes = 6 workers
│  - Consumes from RabbitMQ       │  Calculates build costs + sells prices
│  - Fetches ESI sell prices      │  Writes to build_costs hypertable
│  - Stores results in TimescaleDB│
└──────────┬──────────────────────┘
           │
           ▼
┌─────────────────────────────────┐
│  Pare-Down Job (K8s CronJob)    │  Hourly
│  - Filters build_costs ≥ 20%    │  Writes to pruned_list table
│  - Runs after compute completes │
└──────────┬──────────────────────┘
           │
           ▼
┌─────────────────────────────────┐
│  Reset Job (K8s CronJob)        │  Daily at 00:00 UTC
│  - Truncates pruned_list        │  Fresh start each day
└─────────────────────────────────┘
```

---

## 2. Components

### 2.1 Market Fetcher (K8s CronJob)

**Purpose:** Fetch market orders for all tech 2 and large tech 1 build input items from ESI API (Forge & Sinq Laison regions).

**Schedule:** Every 5 minutes  
**Language:** Go

**Behavior:**
1. Query EVE item database for all buildable items (tech 2 + large tech 1)
2. For each region (Forge, Sinq Laison):
   - Fetch all buy/sell orders for buildable input materials items via ESI
   - Aggregate prices (best buy, best sell per item)
3. Insert into `market_orders` hypertable
4. HTTP POST to API endpoint `/fetch_finished` with region + timestamp

**Input:** ESI API  
**Output:** HTTP POST to API; data stored in TimescaleDB `market_orders`

---

### 2.2 API Server (Go Service)

**Purpose:** Central control plane. Accept signals, queue compute tasks, expose opportunity data to UI/corp members.

**Deployment:** K8s Deployment (1 replica initially, scale as needed)

**Endpoints:**

#### POST `/fetch_finished`
- **Input:** `{ region: string, timestamp: unix_epoch }`
- **Behavior:**
  1. Query `pruned_list` table
  2. If pruned_list is empty → fetch all buildable items from EVE item DB
  3. If pruned_list exists → use only those items
  4. For each item: Query `market_orders` for latest prices in that region
  5. Bundle: item_id, region, timestamp, buy_price, sell_price, materials (from EVE DB)
  6. Publish each bundle to RabbitMQ queue `eve-calc-tasks`
- **Response:** `{ queued: int, region: string }`

#### GET `/opportunities`
- **Query Params:** `?region=Forge&limit=50&offset=0`
- **Behavior:** Query `pruned_list` JOIN `build_costs` to show current opportunities
- **Response:**
  ```json
  {
    "opportunities": [
      {
        "item_id": 587,
        "item_name": "Rifter",
        "region": "Forge",
        "build_cost": 2450000,
        "sell_price": 3100000,
        "profit": 650000,
        "margin": 0.265,
        "buildable_at": "2026-04-09T19:30:00Z"
      }
    ],
    "total": 150
  }
  ```

#### POST `/assign`
- **Input:** `{ corp_member_id: string, item_id: int, region: string }`
- **Behavior:** Insert into `assignments` table
- **Response:** `{ assigned_at: unix_epoch }`

#### GET `/assignments/:member_id`
- **Behavior:** Query `assignments` for a corp member
- **Response:**
  ```json
  {
    "member_id": "alice_1",
    "assignments": [
      {
        "item_id": 587,
        "item_name": "Rifter",
        "region": "Forge",
        "assigned_at": "2026-04-09T18:00:00Z"
      }
    ]
  }
  ```

#### DELETE `/assign/:member_id/:item_id/:region`
- **Behavior:** Remove assignment
- **Response:** `{ deleted: bool }`

**Dependencies:**
- EVE item database (imported)
- TimescaleDB connection
- RabbitMQ connection
- ESI API access (for validation, optional)

---

### 2.3 Compute Worker (K8s Deployment)

**Purpose:** Calculate build costs and store results. Pass sales price from input onto the output.

**Deployment:** 3 replicas (2 per node × 3 nodes, subject to affinity rules)

**Behavior:**
1. Consume message from RabbitMQ queue `eve-calc-tasks` (auto-ack on success)
2. Message format:
   ```json
   {
     "item_id": 587,
     "region": "Forge",
     "materials": [
       { "type_id": 34, "quantity": 2 },
       { "type_id": 35, "quantity": 50 }
     ],
     "timestamp": 1712696400
   }
   ```
3. Calculate build cost:
   - For each material: `quantity × latest_market_orders.buy_price`
   - Account for invention costs (4/4/4 character, flat rate per item type, configurable)
   - Sum all: `total_build_cost = Σ(material_costs) + invention_cost`
4. Fetch current sell price from ESI for the item in that region
5. Calculate profit: `profit = sell_price - total_build_cost`
6. Calculate margin: `margin = profit / sell_price`
7. Insert into `build_costs` hypertable:
   ```
   (item_id, region, total_build_cost, sell_price, profit, margin, timestamp)
   ```
8. ACK message on success; NACK + requeue on failure

**Config:**
- `INVENTION_COST`: Fixed ISK per item (configurable per tech level)
- `RMQ_URL`: RabbitMQ connection string
- `PG_DSN`: TimescaleDB connection
- `ESI_TIMEOUT`: ESI request timeout (default 10s)

---

### 2.4 Pare-Down Job (K8s CronJob)

**Purpose:** Filter opportunities to only those with ≥20% profit margin.

**Schedule:** Hourly (after compute workers complete; e.g., `0 * * * *`)

**Behavior:**
1. Query `build_costs` for the current hour
2. Filter: `WHERE margin >= 0.20`
3. Replace `pruned_list` table:
   - TRUNCATE old entries
   - INSERT filtered items (item_id, region, pared_at=now())
4. Log: `{ items_pared: int, below_threshold: int }`

**No output to UI** — just updates the table used by `/fetch_finished`.

---



## 3. Data Model (TimescaleDB on thehog)

### 3.1 `market_orders` (Hypertable)

Stores raw market order snapshots. Immutable; used for historical analysis.

```sql
CREATE TABLE market_orders (
  time TIMESTAMPTZ NOT NULL,
  region TEXT NOT NULL,
  item_id INT NOT NULL,
  order_id INT NOT NULL,
  buy_price NUMERIC NOT NULL,
  sell_price NUMERIC NOT NULL,
  UNIQUE (order_id, time)
);

SELECT create_hypertable('market_orders', 'time', if_not_exists => TRUE);
CREATE INDEX ON market_orders (region, item_id, order_id, time DESC);
```

---

### 3.2 `build_costs` (Hypertable)

Stores computed build costs and profit margins. One row per item/region per compute cycle.

```sql
CREATE TABLE build_costs (
  time TIMESTAMPTZ NOT NULL,
  item_id INT NOT NULL,
  region TEXT NOT NULL,
  total_build_cost NUMERIC NOT NULL,
  sell_price NUMERIC NOT NULL,
  profit NUMERIC NOT NULL,
  margin NUMERIC NOT NULL  -- profit / sell_price
);

SELECT create_hypertable('build_costs', 'time', if_not_exists => TRUE);
CREATE INDEX ON build_costs (region, margin DESC, time DESC);
CREATE INDEX ON build_costs (item_id, region, time DESC);
```

---

### 3.3 `pruned_list` (Regular Table)

Current day's opportunities. Populated by pare-down job. Query by `date` to search backwards through historical pruned lists.

```sql
CREATE TABLE pruned_list (
  item_id INT NOT NULL,
  region TEXT NOT NULL,
  date DATE NOT NULL,
  pared_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (item_id, region, date)
);

CREATE INDEX ON pruned_list (date DESC);
CREATE INDEX ON pruned_list (pared_at DESC);
```

---

### 3.4 `assignments` (Regular Table)

Corp member assignments to opportunities.

```sql
CREATE TABLE assignments (
  id SERIAL PRIMARY KEY,
  corp_member_id TEXT NOT NULL,
  item_id INT NOT NULL,
  region TEXT NOT NULL,
  assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (corp_member_id, item_id, region)
);

CREATE INDEX ON assignments (corp_member_id);
CREATE INDEX ON assignments (assigned_at DESC);
```

---

## 4. Configuration & Secrets

### 4.1 ConfigMap: `eve-calc-config`

```yaml
data:
  BUILDABLE_TECH_LEVELS: "2,1"  # Tech 2 and large Tech 1
  PROFIT_THRESHOLD_MARGIN: "0.20"  # 20% minimum
  INVENTION_COST_TECH2: "50000000"  # ISK, per item
  INVENTION_COST_LARGE_T1: "0"  # No invention for large T1
  ESI_TIMEOUT_SECONDS: "10"
  MARKET_FETCHER_INTERVAL: "5m"
  PARE_DOWN_SCHEDULE: "0 * * * *"  # Hourly
  RESET_SCHEDULE: "0 0 * * *"  # Daily at 00:00 UTC
```

### 4.2 Secrets: `eve-calc-secrets`

```yaml
stringData:
  PG_DSN: "postgres://user:pass@thehog:5432/eve_calc?sslmode=disable"
  RMQ_URL: "amqp://user:pass@thehog:5672/"
  ESI_API_KEY: "<optional, if ESI requires auth>"
```

---

## 5. RabbitMQ Queue Specification

### Queue: `eve-calc-tasks`

- **Type:** Durable, persistent queue
- **Consumers:** 6 workers, round-robin
- **Message Format:**
  ```json
  {
    "item_id": 587,
    "region": "Forge",
    "materials": [
      { "type_id": 34, "quantity": 2, "market_price": 1500000 },
      { "type_id": 35, "quantity": 50, "market_price": 250000 }
    ],
    "fetched_at": 1712696400
  }
  ```
- **TTL:** 1 hour (auto-purge stale tasks)
- **Max retries:** 3 (NACK → requeue up to 3x, then dead-letter)

### Dead-Letter Queue: `eve-calc-tasks-dlq`

Captures tasks that failed 3+ times. Monitored for alerts.

---

## 6. Kubernetes Manifests Structure

```
k8s/eve-calc/
├── namespace.yaml
├── configmap.yaml
├── secrets.yaml (to be encrypted with sealing secret)
├── api-server-deployment.yaml
├── compute-worker-deployment.yaml
├── market-fetcher-cronjob.yaml
├── pare-down-cronjob.yaml
├── reset-cronjob.yaml
├── api-service.yaml
└── README.md (deployment guide)
```

---

## 7. External Dependencies

- **ESI API:** Market orders, item data
- **EVE Item Database:** Imported copy (static, one-time load or periodic refresh)
- **TimescaleDB:** Running on thehog (192.168.50.248:5432)
- **RabbitMQ:** Running on thehog (192.168.50.248:5672)
- **Caddy:** Exposes API at api.eve-calc.r3r4um.online (or similar)

---

## 8. Future Iterations

- [ ] Multi-character invention profiles (different skills affect cost)
- [ ] Dynamic thresholds per corp/player
- [ ] Sell price trending (predict future prices)
- [ ] Jump costs & logistics into profit calc
- [ ] Web UI dashboard (React/Vue)
- [ ] Discord bot for notifications (new opportunities, assignments)
- [ ] Tax rates per structure

---

## 9. Notes

- **Buildable items:** Tech 2 + large Tech 1 only. Full list imported from EVE database at pod startup.
- **Dedupe:** Rifter appears twice compute cycle (every 5 mins). This is expected; pare-down filters by margin, not dedup.
- **Regions:** The Forge (Jita) & Sinq Laison (Dodixie) for now. Expandable to other regions later.
- **Margin calc:** `margin = (sell_price - build_cost) / sell_price` (0.0–1.0)
- **Profit filter:** ≥0.20 (≥20% margin). Conservative to start; adjust as data informs.
