Sparehand

Docs › Credits: letting members pay for what they use

Credits: letting members pay for what they use

Beta. Members and credits work and are part of the licence, but they are newer than the core and may change between versions. They need the dashboard published on Tailscale (see Requirements).

People you add as members on the dashboard's People tab (remote-access.md) use your machines' AI models, make videos in Studio and run Crew tasks with prepaid credits. You sell them credits however you like (cash, a bank transfer, a gift) and add them on the People tab; what they use is taken from their balance, answer by answer and call by call. 1 credit = 1 cent. Owners are never charged.

What a member sees

At the top of their Machines tab, an Account card: credits left, what they used this month and in all, the last few charges, the price list, and their API keys with the address apps use them at. The header shows their balance next to their name, and it moves after every answer.

When a member has no credits left, the chat answers "You have no credits left … ask the owner for more", and their API keys stop working, until you add some.

Monthly statement on the Account card opens their statement (below) for this month or an earlier one, with Download CSV. A suspended member sees a line saying so.

Adding credits

On the People tab, every member shows their balance and usage. Add credits asks for the number of credits and a note (what they paid, say); the note stays in their ledger. Credits can only be added, never taken away: a mistake is corrected by adding the difference next time, or by hand in the ledger (below).

Low on credits. When a charge takes a member to 10% of the last credits you added or less (50 left of a 500 grant, say), your devices get a notification, Low on credits: Sam, with what is left. It comes once for that grant: the next one you add starts it again. (low.json in their folder remembers which grant it was for, across restarts.) Notifications have to be on for this device first (sessions.md).

Refunds, suspending, statements

Refund… next to a member gives back credits they were charged (an answer that went wrong, a task that stopped early): it asks how many, and adds a refund to their ledger. It is never more than they were charged in all, less the refunds given already; a refund that would go past that is refused with the most it can be. A refund counts against their usage, so "used this month" goes down by it.

Suspend stops a member for now, after you confirm it: they can still open the dashboard and read their own credits and statement, but everything else answers "Your access is suspended for now: ask the owner" (the chat, Studio, Crew, their saved chats), and their API keys are refused on the hub. What is already running (an answer, a Crew part, a render) finishes and is charged as usual; their balance, ledger, chats, tasks and productions stay as they are. The row shows Suspended, and Resume lets them back in. Only members can be suspended. ({"action": "suspend", "id": "…", "note": "…"} and "resume" on POST /api/people; "refund" takes cents, and ref for one charge.)

Statement opens a month of a member's credits: the balance it opened and closed with, what you added, what was charged, what was refunded, the holds put aside and given back, what was still held at the end of the month (a task that was running), and every entry with the balance after it. Pick another month at the top; Print prints just the statement, and Download CSV saves it as credits-NAME-YYYY-MM.csv, one row an entry (date,kind,credits,balance,note,ref) between an opening and a closing balance row. Months follow UTC, as the ledger's times do. The same is at GET /api/people/statement?id=ID&month=YYYY-MM (owners; &format=csv for the file), and a member reads their own at GET /api/me/statement.

What things cost

Prices are in ~/.config/sparehand/app.json under prices, in cents per million tokens, by the model's size class. The class is read from the parameter count in the model's name (qwen3:8b, llama3.1:70b, gemma3n:e4b, qwen3:30b-a3b counts as 30); a name with no size is charged as the largest class. The defaults, which apply when prices is missing:

{
  "prices": {
    "minimum": 1,
    "ai": {
      "small":  { "upTo": 9,    "input": 20,  "output": 80 },
      "medium": { "upTo": 35,   "input": 60,  "output": 240 },
      "large":  { "upTo": null, "input": 200, "output": 800 }
    },
    "jobs": { "perMinute": { "default": 0 } }
  }
}

The counts come from the model server itself: Ollama reports prompt_eval_count and eval_count in the last chunk of every answer, and OpenAI-style requests report usage (a streamed /v1 request from a member is asked to include it). An answer that ends without counts (the machine fell asleep mid-answer) is charged its estimate, and a request that never reached a machine costs nothing.

Restart the app after changing app.json (quit Sparehand from its menu-bar icon and open it again).

API keys and the hub

A member's apps can use your models directly, through the AI hub (ai.md): on the Account card, New key makes a key (sh_ and 32 characters) that is shown once; the dashboard keeps only its hash. The app sends it as Authorization: Bearer sh_…, or in its "API key" field, to the hub's address, which works as an Ollama server and (under /v1) an OpenAI-compatible one. Members can't download or delete models with a key.

The hub answers only on the hub computer itself (localhost:11434), so publish it on your tailnet next to the dashboard:

tailscale serve --bg --https=8443 http://127.0.0.1:11434

The Account card then shows https://my-hub.tail1234.ts.net:8443 as the address (your dashboard's host on port 8443; set "aiAddress" in app.json if you publish it elsewhere, or "aiPublicPort" for another port). Requests without a key keep working as before, for apps on the hub computer only. A key that was revoked, or mistyped, gets 401; a member with no credits gets 402.

The ledger

Each person's record is ~/.local/state/sparehand/people/ID/ledger.jsonl (the folder readable by you only), one JSON line per entry, never rewritten:

{"t":"2026-10-18T12:00:00.000Z","kind":"grant","cents":500,"ref":null,"note":"paid 5 € cash"}
{"t":"2026-10-18T12:01:00.000Z","kind":"hold","cents":-1,"ref":"3f9a1c2b4d5e","note":"chat · qwen3:8b"}
{"t":"2026-10-18T12:01:04.000Z","kind":"release","cents":1,"ref":"3f9a1c2b4d5e","note":""}
{"t":"2026-10-18T12:01:04.000Z","kind":"charge","cents":-2,"ref":"3f9a1c2b4d5e","note":"qwen3:8b · 1,840 in, 612 out"}

The balance is the sum of cents. A grant adds; before a metered request a hold puts an estimate aside (never more than is there), and when the answer ends the hold is released and the real cost charged; a refund gives a charge back. A charge may be a little more than its hold (the model answered longer than guessed), so a balance can go below zero by one answer's worth, and the next request is refused. A hold left behind by a restart is released when the app starts. To correct something by hand, add a line of your own (a grant or a refund) and restart the app; don't edit earlier lines. Removing a person keeps their ledger.

Studio

When Studio is on (studio.md), members make videos on their credits. The storyboard a member approves shows its price: the voice (per 1,000 characters), the Director's remaining calls, clips made with AI and the machine minutes, each at your cost times markup (machine time at jobs.perMinute, no markup). Approving holds that amount; while it is made, every step counts what it really used (characters spoken, pictures and tracks made, seconds of clips, the Director's tokens at the provider's list price, minutes on each machine); when the run ends the hold is settled: a release of the hold and one charge for what was used, up to 1.5× the estimate, with a note saying what for:

{"t":"…","kind":"hold","cents":-20,"ref":"…","note":"Studio · Rainy day · making it"}
{"t":"…","kind":"release","cents":20,"ref":"…","note":""}
{"t":"…","kind":"charge","cents":-14,"ref":"…","note":"Studio · making it · 1,840 characters spoken · Director 24,000 tokens · 1.2 machine minutes"}

Planning the storyboard is a small run of its own (the Director's call), and so are the pictures made while the storyboard waits for approval (the made pictures, the clips made then, a minute on a machine for each made on one): held when they start, charged for what they used when they are there. A production's pictures are spread over every maker that can make them (see studio.md), so the hold counts each picture at the maker it goes to: one on your machine is free but holds a machine minute, one made with Gemini holds its price per picture; each made picture is then counted at what it really cost. Minutes on a paid machine (a cloud worker, or one with a price from sparehand set NAME price, used only when the owner ticked it on the storyboard) count in the owner's cost line at its price an hour divided by 60; a member pays them at jobs.perMinute for that machine, or, without an entry for it, that same figure in credits. A step the hold and the balance can't cover isn't started: the production stops cleanly, keeps what was made, and Resume carries on with a new hold after you add credits (a storyboard whose pictures the balance can't cover waits, saying so; the approval makes the rest).

Usually you approve each member's request first (Studio's Members' productions setting, You approve each request): in the request you choose its Director, your own Master (its tokens are yours, never charged to them) or a writer billed per use (charged to them as above), and its price: the suggested price, your costs × markup + the machine minutes at jobs.perMinute + a flat directing fee (directingFee, 20 credits by default), or a number of your own, which is then the whole charge whatever the production uses. The fee goes into the hold when making it starts and is charged with it, named in the note; a price you set is charged in full with the first run:

{"t":"…","kind":"charge","cents":-39,"ref":"…","note":"Studio · making it · 320 characters spoken · 0.5 machine minutes · directing fee 25"}
{"t":"…","kind":"charge","cents":-60,"ref":"…","note":"Studio · making it · 320 characters spoken · 0.5 machine minutes · the price set by the owner"}

With the setting on Billed per use, no queue, a member's production is planned at once on a Director billed per use (the Gemini key, or a Crew agent with its own API key), never your subscription, with no fee. Each production shows you its cost, price and margin (jobs.costPerMinute, cents, counts machine minutes at your own cost when set), and Studio sums the members' month up.

A declined request charges nothing. Owners are never charged.

Crew

When Crew is on (crew.md), members run tasks of their own on their credits, in a workspace of their own on the hub and on agents billed per use only (never your Claude or ChatGPT subscription). Every call an agent or the Master makes is charged by the tokens its tool reports, at your cost for that provider and model times markup. Your costs are in app.json under prices.tokens; the defaults, which apply when it is missing:

{
  "prices": {
    "tokens": {
      "anthropic": { "fable": [1000, 5000], "opus": [400, 2000], "sonnet": [200, 1000], "haiku": [10, 50], "*": [400, 2000] },
      "kimi":      { "*": [60, 250] },
      "moonshot":  { "*": [60, 250] },
      "gemini":    { "flash": [50, 300], "pro": [200, 1200], "*": [200, 1200] },
      "xai":       { "grok-3-mini": [30, 50], "grok-4": [300, 1500], "*": [300, 1500] },
      "mistral":   { "codestral": [30, 90], "mistral-small": [10, 30], "mistral-medium": [40, 200], "mistral-large": [200, 600], "*": [200, 600] }
    },
    "crew": { "floor": 100 }
  }
}

A task's ledger:

{"t":"…","kind":"hold","cents":-100,"ref":"…","note":"Crew · Add tests · the estimate"}
{"t":"…","kind":"charge","cents":-36,"ref":"…","note":"Crew · Add tests · planning · Claude API (claude-sonnet-5-5) · 1,160 tokens"}
{"t":"…","kind":"release","cents":100,"ref":"…","note":""}
{"t":"…","kind":"hold","cents":-64,"ref":"…","note":"Crew · Add tests · the rest of the estimate"}

Each call is a charge as soon as its tool reports the tokens, saying which step it was for (planning, a part by its title, a review, the final review, an agent's question, a merge conflict), and the hold is then set to what is left of the estimate (a release and a new hold). While the plan waits for approval, and when the task ends, the hold is released. Before each part, the hold and the balance must cover about what a part costs in that task; when they don't, the task stops cleanly before it and Resume holds anew once you have added credits. Owners are never charged.

Payments (buying credit packs online) and charging for a member's own jobs aren't there yet; the ledger and prices are made so they can be added without changing what is written here.