Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions gateway/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,48 @@ flash without being told would draw wrong conclusions and blame the model.

---

## The status page

`freeseek.1lm.io/` is the dashboard, served by the gateway itself from
`go:embed` — one binary, and the page can never be a different version
from the JSON it reads. It polls `GET /v1/status`, which is public,
unauthenticated and cached for three seconds.

What that document says and what it refuses to say are both deliberate:

| published | withheld | why |
|---|---|---|
| credit as a **percentage** | dollars, account balance | "$0.19 of $0.25" is a progress bar for whoever wants to trip the breaker |
| subject ids **truncated to 6 chars** | whole subject ids | a whole id can be matched against the one in someone's `free.json` |
| **per-country** request counts | anything per-IP, ever | an aggregate is a fact about the service, not about a person |
| token counts, tok/s, live subjects | prompts, completions | the promise in `deepseek free` is the promise here |

Exact money, per-key health and the full subject table live behind
`GET /admin/status` with the operator token.

Metrics live in package `stats`: a ring of per-second buckets, bounded
maps, everything in memory and lost on restart. That is the right trade —
losing it costs a graph, and it keeps observability from ever becoming a
second, weaker copy of the money.

## The key pool

One key was a single point of failure with a hard floor. Package
`keyring` holds several, rotates per request, and retires a key the
moment DeepSeek answers 401 or 402 on it — the authority on "this key is
done" is upstream, not our own ledger, because a donated key may be
funding something else as well.

A secret never leaves the package. Every accessor returns a fingerprint
(last four characters plus a truncated hash), which tells two keys apart
in a dashboard and is useless to anyone who steals the output.

Donations are added by an operator through `POST /admin/keys` and
persisted, so a gift survives a restart and lands without a deploy.
**There is deliberately no public form.** A service that collects other
people's API keys over the open internet is a phishing lesson with a nice
stylesheet; the donation path is a private message to a human.

## What we deliberately did not build

- **A user table.** Stateless tokens plus daily counters. Nothing to
Expand Down
15 changes: 12 additions & 3 deletions gateway/cmd/dsgate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const usage = `dsgate — the free tier for the deepseek CLI
Configuration is entirely environment variables:

DSGATE_UPSTREAM_KEY DeepSeek API key to spend (required)
DSGATE_UPSTREAM_KEYS more keys, comma separated; the pool rotates
DSGATE_UPSTREAM_BASE_URL upstream root (https://api.deepseek.com)
DSGATE_ADDR listen address (:8787)
DSGATE_STATE_DIR journal, secret, revocations (./state)
Expand Down Expand Up @@ -102,8 +103,14 @@ SIGHUP re-reads <state>/revoked.txt without dropping connections.
`

func run() error {
key := env("DSGATE_UPSTREAM_KEY", os.Getenv("DEEPSEEK_API_KEY"))
if key == "" {
// One key or many. The pool is what lets a donated key extend the
// service without a restart, and what lets an emptied one retire
// itself instead of taking the whole free tier down with it.
keys := envList("DSGATE_UPSTREAM_KEYS")
if k := env("DSGATE_UPSTREAM_KEY", os.Getenv("DEEPSEEK_API_KEY")); k != "" {
keys = append([]string{k}, keys...)
}
if len(keys) == 0 {
return errors.New("DSGATE_UPSTREAM_KEY is not set; there is nothing to spend")
}

Expand Down Expand Up @@ -150,8 +157,10 @@ func run() error {

cfg := server.Config{
UpstreamBaseURL: env("DSGATE_UPSTREAM_BASE_URL", "https://api.deepseek.com"),
UpstreamKey: key,
UpstreamKeys: keys,
KeyStatePath: filepath.Join(stateDir, "donated-keys.json"),
Model: env("DSGATE_MODEL", "deepseek-v4-flash"),
Version: version,
MaxBodyBytes: int64(envInt("DSGATE_MAX_BODY_BYTES", 131072)),
MaxTokens: envInt("DSGATE_ANON_MAX_TOKENS", 4096),
MaxInflight: envInt("DSGATE_MAX_INFLIGHT", 8),
Expand Down
Loading