# Albion Black Market Flipper

A local web dashboard that finds profitable "buy in a royal city, sell to the
Black Market" flips in Albion Online.

## How it works

- Prices come from the [Albion Online Data Project](https://www.albion-online-data.com/)
  (AODP), a public, community-run API that aggregates player-submitted market
  data. No game files are read or modified — this is the same kind of price
  API used by tools like AlbionOnline2D and the in-game "Black Market Tool" apps.
- For every item and quality, it looks at:
  - **Buy price**: the lowest instant-buy ("sell order") price across your
    selected royal cities (Bridgewatch, Caerleon, Fort Sterling, Lymhurst,
    Martlock, Thetford).
  - **BM instant-sell price**: the Black Market's highest standing buy order
    (`buy_price_max`), i.e. what you'd get by instantly delivering the item
    there — guaranteed, no waiting.
  - **BM sell order (ask) price**: the Black Market's current lowest sell
    order (`sell_price_min`) — the going rate if you posted your own sell
    order instead of instant-selling. Usually higher, but **not instant or
    guaranteed**: it has to actually be competitively priced and then wait
    for a buyer.
- Instant-sell profit = `BM instant-sell price × (1 − sell tax) − city buy price`.
  Sell order profit = `BM ask price × (1 − sell tax − setup fee) − city buy
  price` — the setup fee is a separate, non-refundable cost charged upfront
  on the listed price the moment you post the order, whether or not it ever
  sells. ROI% is profit divided by the buy price, for each.
- Sell tax defaults to 8% / setup fee 2.5% (non-Premium). Switch the
  dropdown to Premium (4% / 1.5%) if you have it active.
- **Sell order asks are easy to fake and cost the poster almost nothing** —
  someone can list an item at an absurd price with no real intent to sell
  at it, since the setup fee is small relative to a huge listed price. This
  shows up as suspiciously round, extreme "Sell Order (ask)" values paired
  with ordinary (or even negative) instant-sell profit on the same row.
  Sanity-check any sell-order flip that looks too good, the same way you
  would for any other stale-or-manipulated market data.

## Running it

**Day to day, just open `https://zed.tailf0661c.ts.net` in a browser** —
that's the real, permanently-running instance (on a Raspberry Pi, see
"Sharing this with friends" below), nothing to start locally for that.

Running a copy on *this* PC is now only for solo/offline use or local
development, not the primary way to use this:

```bash
node server.js
```

then visit `http://localhost:8787`. You can also open [index.html](index.html)
directly in a browser for basic REST-only use, but **Live capture and the
private data log below both require `server.js`** — it's what hosts the
local API they talk to, and the Data Client refuses websocket connections
from a bare `file://` page anyway.

[start-hidden.vbs](start-hidden.vbs) exists for this local copy too (edit
the hardcoded `SHARED_KEY` line inside if you want it, then copy it to
`shell:startup` — Win+R) but isn't installed by default anymore now that
the Pi handles always-on hosting.

## Using it

1. Pick your server region (Americas/Europe/Asia) and premium status.
2. Click **Fetch prices**. This queries ~9,500 items in batches of a few
   hundred at a time (the API has a URL-length limit), so a full refresh
   takes roughly 30–60 seconds. Progress is shown under the button.
3. Sort by clicking any column header (defaults to Profit, descending).
4. Use the filters to cut noise:
   - **Min profit / Min ROI%** — hide flips too small to bother with.
   - **Max data age (hrs)** — hide flips based on stale quotes. Black Market
     buy orders in particular can sit unfilled for a long time; a huge
     "profit" on a quote that's 3 days old is usually a trap (the order may
     already be gone or about to expire).
   - **Source cities / Qualities** — narrow down where you're willing to shop
     and which item qualities you care about.

## Live capture (fresher data for items you personally check)

The public REST snapshot is only as fresh as the last time *someone,
anywhere* browsed that item's market. For a much fresher read on the
specific flips you're actually about to act on, you can connect the
dashboard to the official [Albion Data Client](https://github.com/ao-data/albiondata-client)
running on your own machine — it's the same tool that feeds the public
AODP pool, sniffing your own outgoing game traffic (no memory reading, no
game files touched) and, with a small config change, also broadcasting
what it sees locally over a websocket the instant you open a market panel
in-game.

Setup:

1. Download and run the client from the [releases page](https://github.com/ao-data/albiondata-client/releases).
2. Copy [albiondata-client-config.yaml](albiondata-client-config.yaml) into
   the same folder as the client executable, rename it to `config.yaml`,
   and restart the client. This just adds:
   ```yaml
   EnableWebsockets: true
   AllowedWebsocketHosts:
     - localhost
   ```
3. Make sure you're running this dashboard via `node server.js` (not by
   opening `index.html` directly) — the client refuses websocket
   connections from a bare `file://` page.
4. In the dashboard, open the **Live capture** panel near the top and
   click **Connect**. It defaults to `ws://localhost:8099/ws`, which is
   where the client listens.
5. Play normally. Every time you open a market/Black Market panel for an
   item, that capture streams into the dashboard and overrides the
   relevant row(s) — tagged with a green **LIVE** badge — with your
   just-observed price and an age of a few seconds, regardless of what the
   public snapshot said.

This only ever improves data for items you actually look at in-game; it's
not a way to get fresher data on the other ~9,000 items you haven't
checked — that still depends on the wider community's activity.

## Your own private data log (strictly local, never published)

Everything captured through Live Capture is also written to a local,
append-only log on this machine — `data/live-log.jsonl`, plus
`data/live-latest.json` as a fast-loading snapshot. This is **your own
data project**, separate from AODP:

- It's built entirely from your own in-game market views.
- `server.js` only ever writes to these files on disk; it makes no
  outbound network requests of its own. Nothing captured here is
  transmitted anywhere.
- It accumulates across sessions — reopen the dashboard days later and
  your prior captures are still there overriding rows (tagged **LIVE**)
  until the REST snapshot naturally supersedes them or you re-capture
  fresher ones.
- It sits *alongside* the public AODP reads, not instead of them — you
  still get full-catalog coverage from the public API, plus a private
  layer of higher-confidence data for whatever you've personally checked.

To keep the *upstream* Data Client itself from also uploading your
captures to the public AODP pool, run it with `-d` (`DisableUpload`) —
see [run-private-capture.bat](run-private-capture.bat) (edit the exe path
inside first). This flag only stops outbound uploads; the local websocket
that feeds this dashboard's Live Capture panel keeps working exactly the
same, since that's a separate, unconditional local broadcast in the
client's own code.

*(We also looked into self-hosting a private replacement for the entire
AODP server stack — the official server code, [albiondata-server-rails](https://github.com/ao-data/albiondata-server-rails),
literally says "Don't use this, it's a work in progress that isn't ready
yet" in its own README, so that's not a realistic option today. The local
log above gets you the same practical outcome — your own data, never
shared — without depending on unfinished infrastructure.)*

## Trying shared mode locally first

Before setting up a VPS, you can rehearse the whole shared-mode flow on
this one PC: run [run-shared-test.bat](run-shared-test.bat), then open
`http://localhost:8788` in two separate browser windows (use one normal +
one private/incognito window so they don't share saved settings). Give
each a different name in **Your name**, and the same shared key
(`local-test-key`) in **Shared key**. Capture something as one, and the
other will pick it up next time it loads the shared snapshot — same
mechanism `DEPLOY.md` sets up for real, just on `localhost:8788` with its
own isolated `data-test/` folder instead of a VPS, so it never touches
your real solo data in `data/`.

## Sharing this with friends

The private data log above can be a *shared* private log instead of a
solo one — you check one market, a friend checks another, and both
captures merge into one dataset that's still never published to public
AODP. See [DEPLOY.md](DEPLOY.md) for the full setup (this exact one:
a Raspberry Pi + Tailscale Funnel, and the VPS alternative). In short:

- One machine hosts this app permanently — set up as a systemd service so
  it survives reboots — instead of everyone using their own `localhost`.
- Everyone else just opens that URL in a browser — no local install of
  this app needed. To also *contribute* captures (not just read), they run
  the standalone capture tool below.
- The `SHARED_KEY` environment variable on the host gates every request;
  everyone enters the same passphrase in the dashboard's **Shared key**
  field. Without it, reads and writes are rejected (401) — this is what
  keeps the group's data private from anyone else who finds the URL.
- The **Your name** field tags your captures so the group can see who
  found what — purely a courtesy label, not an account system.

**Currently live:** `https://zed.tailf0661c.ts.net`, shared key
`hj6KF5U1jVunpS7g` — hosted on a Raspberry Pi 4 as a systemd service
(auto-restarts, survives reboots) with a Tailscale Funnel for the stable
public URL. Automatic retention keeps it from filling the Pi's disk: live
captures older than 14 days are dropped, and the raw capture log is capped
at 25MB (both configurable via env vars in the systemd unit — see
DEPLOY.md). Full details, including how this was set up, in
[DEPLOY.md](DEPLOY.md).

### Giving friends a capture tool

[friend-capture-pack.zip](friend-capture-pack.zip) is a ready-to-send,
no-install, no-admin-rights bundle: the official Albion Data Client
binary, pre-filled with the config above, and a launcher that hardcodes
`-d` — so whoever runs it contributes to this shared dashboard only,
never the public AODP. Send the zip along with the dashboard URL and
shared key; `README-FRIENDS.md` inside walks them through the rest.

## Important caveats (read before hauling goods across the map)

- **Data can be stale or wrong.** AODP relies on players submitting price
  data via addons/apps. Low-traffic items may show old quotes. Always check
  the **Data Age** column and consider re-verifying big flips in-game before
  committing.
- **No transport cost/risk modeling.** This tool ignores travel time,
  weight/carry capacity, and the very real risk of getting ganked hauling
  valuable goods to the Black Market (which sits in dangerous territory).
  Factor that in yourself — a 500-silver-profit low-tier flip probably isn't
  worth the trip; a big juicy artifact flip might still not be worth it if a
  gank squad camps the Black Market entrance.
- **Black Market buy orders are finite.** `buy_price_max` reflects the best
  standing order right now. If you're moving a large quantity, later units
  may sell at lower prices as you fill through the order book — this tool
  only shows the best single price point, not order depth.
- **Buy side is always instant-fill.** The city buy price always assumes
  filling an existing sell order instantly, with no setup fee — the tool
  doesn't model posting your own (lower) buy order and waiting for it to fill.

## Files

- `index.html` / `style.css` / `app.js` — the app.
- `items-data.js` — static item ID → English name lookup (~9,500 items,
  sourced from the public [ao-bin-dumps](https://github.com/broderickhyman/ao-bin-dumps)
  project so the tool doesn't need a separate network call just to show names).
- `server.js` — zero-dependency server: serves the static files and hosts
  the private data log API (`/api/live-log*`). Solo mode by default; set
  `SHARED_KEY` (env var) to require it on every request, for shared use.
- `DEPLOY.md` — how to host this for a friend group instead of solo.
- `run-shared-test.bat` — runs a second, isolated instance (port 8788,
  its own `data-test/` folder) to rehearse shared mode locally.
- `albiondata-client-config.yaml` — drop-in config for the Albion Data Client to enable the Live capture feature (see above).
- `run-private-capture.bat` — launches the Data Client with public uploads disabled.
- `cloudflared.exe` / `run-tunnel.bat` — quick-tunnel option from this PC, superseded by the Pi but still useful for a temporary test (see `DEPLOY.md`).
- `friend-capture-pack/` / `friend-capture-pack.zip` — standalone, no-install capture tool to send to friends (see "Sharing this with friends" above).
- `data/` — created automatically; your private capture log lives here. Back it up or delete it freely, it's just JSON.

## Ideas for later

- Add Brecilien and outland hideout markets.
- Show Black Market order depth (not just best price) for bulk flips.
- A UI for `GET /api/live-log/history` (already returns your logged
  history per item/quality/city) — e.g. a trend sparkline instead of a
  single point-in-time snapshot.
- Add a rough "worth the trip" score factoring in item weight and distance
  from each city to the Black Market.
