{"slug": "share-the-tokens-not-the-api-key", "title": "Share the tokens, not the API key", "summary": "Block's Buzz makes multi-agent workspaces legible by scoping identity rather than permission flags, but the missing primitive is delegating bounded AI capacity without sharing API keys. freeq, an IRC server with cryptographic keys, solves this with a BUDGET command that lets operators set spending limits per channel and agents report signed spend against that budget, enabling consented, bounded, attributable, and revocable capacity loans.", "body_md": "# Share the tokens, not the API key[¶](#share-the-tokens-not-the-api-key)\n\n*2026-07-26*\n\nBlock's [Buzz](https://github.com/block/buzz) made the multi-agent workspace legible\n\nthis month: humans and agents in the same room, every message and workflow step a\n\nsigned event in one log, and agents holding their own keys rather than borrowing a\n\nhuman's. Its README puts the principle better than most specs do: *\"Scoped by identity, not by\npermission flags — the same way you'd scope a teammate.\"*\n\nWhich surfaces the next question. When five agents wake up to work, whose model\n\nallowance are they spending?\n\nPeople and teams increasingly pay for AI capacity that goes unused for long\n\nstretches: subscription allowances, prepaid credits, an organisational budget,\n\na box running local inference. Meanwhile somebody else, or somebody else's agent,\n\nneeds capacity right now and has none. Providers generally don't make that capacity\n\nsafely delegable, and handing over your account credential is usually both\n\nprohibited and reckless.\n\nSo the missing primitive isn't another shared API key. It's the ability to delegate\n\na bounded amount of capacity to a person or an agent you choose, and keep\n\nattribution, revocation, and control while they use it.\n\nfreeq is unusually well-shaped for that problem. [The first post in this\nseries](https://freeq.at/blog/what-is-freeq/) explains the foundation: it's an IRC\n\nserver that speaks the protocol a 1999 client speaks, except every participant holds a\n\ncryptographic key. This post is about what those keys become useful for once agents\n\nstart consuming resources on somebody else's behalf.\n\n(If protocol machinery is not your idea of a good time, the same substrate is rendered\n\nat [FreeqWorld](https://world.freeq.at/) as a multiplayer town: rooms are live\n\nchannels, people and agents keep their DID-derived characters, and federation appears\n\nas travel between independently operated towns.)\n\n## What a loan of capacity would require[¶](#what-a-loan-of-capacity-would-require)\n\nHand someone your credential and you've given them whatever authority that key\n\ncarries, for as long as it stays valid, with no cryptographically attributable\n\nrecord of which jobs were theirs, and no way to withdraw it except rotation that\n\nbreaks everything else using the same key. That isn't lending. It's making them you.\n\nA real loan needs the authority to spend to be its own object:\n\n**consented to** by the party whose capacity is being lent,**bounded** by an amount and a period,- attached to an\n**identity** rather than a secret, **attributable** per job, to the instruction that authorised it,**revocable** without collateral damage,- and\n**narrowing** rather than widening as it's passed along.\n\nThat is a delegation problem, not a billing problem.\n\n## The pieces freeq already had[¶](#the-pieces-freeq-already-had)\n\nAn operator sets a budget in a channel and names who funds it. The sponsor defaults\n\nto whoever issues the command:\n\n```\nBUDGET #research amount=500;unit=credits;period=day;warn=0.8;hard=true\n```\n\nAgents working there report what their work cost, signed, against that budget:\n\n```\n@+freeq.at/sig=ed25519:kid:9f2c…:BASE64\n SPEND #research :amount=3;unit=credits;desc=claude-sonnet-4:1.2k-tokens;task=01JQ…\n```\n\nThat is the protocol shape a loan needs: a named sponsor, someone else's attributed\n\nwork, and a limit that follows the delegation chain. At the start of this work freeq\n\nenforced that limit only over *reported* spend. It did not mediate or debit the\n\nunderlying model capacity. Joining those two systems is what changed while I was\n\nwriting this post.\n\nAround it, the identity rails are real:\n\n**Who the human is.** Accounts are AT Protocol DIDs. You authenticate by signing a\n\nserver challenge with the key in your DID document, so the server holds no password\n\nand didn't issue your identity.**Who the agent is.** An agent has its own keypair, and its creator signs a\n\n`FreeqBotDelegation/v1`\n\ncertificate over the agent's DID and public key. Four\n\nrejection paths are tested: tampered signature, DID mismatch, missing signature,\n\ncreator never registered. That proves one thing precisely, namely that this key was\n\nauthorised by that key, and nothing about who holds the key now or what software\n\nis running.**What was said.** Every client registers an ed25519 key with`MSGSIG`\n\nand signs\n\neach message. The server verifies and relays the signature unchanged rather than\n\nre-signing, and keys are stored append-only by`(did, kid)`\n\nso a signature stays\n\nverifiable after the sender rotates or disconnects.**What can be passed on.** Spend aggregates across the delegation chain instead of\n\nresetting at each new identity, and a child inherits its parent's limit rather than\n\nfalling through to the channel default. Capabilities narrow on delegation: a child\n\ngets the intersection with its parent's set, so an agent can't confer authority it\n\ndoesn't hold. That last one was a bug until this week. The spawn path recorded\n\nwhatever was requested, so an agent holding nothing could create a helper recorded\n\nas holding anything.\n\n## The seam where it stopped[¶](#the-seam-where-it-stopped)\n\nTesting the joins between those systems is where the interesting findings were, and\n\nthe sharpest one is a two-line observation:\n\nThe budget system is metered and unmediated. The model path is mediated and\n\nunmetered.\n\nThat was the state when I started writing this. freeq put itself between a caller and\n\na paid model in exactly one place, its diagnostic interface, where the server holds\n\nthe provider key and callers are authenticated by session bearer to DID, so nobody is\n\nhanded the credential. That path recorded no spend and checked no budget. Meanwhile\n\nthe budget system counted carefully, over numbers agents reported about calls freeq\n\nnever saw. **The one paid resource freeq controlled was the one it didn't count.**\n\nOnce stated that plainly, the separation was impossible to justify. So the two halves\n\nare now joined.\n\n## The join, now that it exists[¶](#the-join-now-that-it-exists)\n\n`POST /api/v1/model/chat/completions`\n\nis an OpenAI-compatible call made *by the\nserver*, charged to a channel budget:\n\n```\nAuthorization: Bearer <session>\n{ \"channel\": \"#research\", \"model\": \"provider/small\",\n  \"messages\": [{ \"role\": \"user\", \"content\": \"…\" }] }\n```\n\nThe caller authenticates as a DID and must be in the channel it names. The budget is\n\nresolved through the delegation chain, spend for the period is totalled, and the\n\ndecision happens *before* dispatch. If the budget is exhausted, the response is `402`\n\nand **no upstream request is made at all** — which is the difference between a limit\n\nand a request to stop. The tests assert that by counting hits on a mock provider, so\n\n\"we refused\" cannot quietly mean \"we asked nicely and it went ahead\".\n\nThe charge is computed from the provider's own token counts rather than the caller's\n\nclaim, priced per model, and recorded against the budget:\n\n```\n\"freeq\": { \"charged\": 4, \"unit\": \"credits\", \"channel\": \"#research\",\n           \"sponsor\": \"did:plc:…\", \"spent_after\": 17 }\n```\n\nTwo details that took a test to get right. An unpriced model is not free — it falls\n\nback to a deliberately expensive default, because \"ask for a model nobody priced\" is\n\nthe obvious way to get unlimited capacity. And a declared `max_tokens`\n\nis billed as\n\noutput in the pre-call estimate, so the last call before a ceiling can't overshoot it\n\nby an unbounded amount. Without a declared maximum, the limit is enforced at call\n\ngranularity and can be passed by one call's cost, which is the honest bound and is\n\nwhat the test pins.\n\nThe credential never leaves the server. So withdrawing capacity means editing a\n\nbudget, not rotating a key and breaking everything else that used it.\n\nThe proxy is not the protocol. It is where the protocol's decision becomes\n\nenforceable. Anyone can put a metering proxy in front of a model. What makes this one\n\nanswer \"should this call happen\" is the context upstream of it: the caller's identity,\n\nchannel membership, the delegation graph, the named sponsor, and the inherited budget.\n\nThe remaining step is to bind the call to the signed job being paid for.\n\n## What is still missing[¶](#what-is-still-missing)\n\nThree gaps, all found by writing tests rather than reading designs:\n\n**Being a sponsor requires no consent.** `sponsor=<did>`\n\naccepts any identity, and\n\nnaming someone other than yourself takes only channel op. That DID is never asked. An\n\noperator can stand up a channel, name an unrelated person as its funder, and have\n\nevery agent's spend attributed to them, and since sponsors are now notified, send\n\nthem the warnings too. Lending is something you agree to.\n\n**Externally incurred spend is still self-reported.** Calls through freeq's model\n\nendpoint are mediated, measured from the provider's token counts, and refused before\n\ndispatch when the budget is exhausted. An agent can still call a model somewhere else\n\nand file a `SPEND`\n\nreport afterwards, and freeq cannot verify that report, detect an\n\nomitted one, or prevent the external call. So the two paths have genuinely different\n\nguarantees: the mediated path enforces a budget, and `SPEND`\n\nis accounting for\n\ncooperative runtimes. `hard=true`\n\nmeans \"no provider call\" on the first and \"please\n\nstop\" on the second.\n\n**Capabilities don't gate operations.** They narrow correctly on delegation now, but\n\nnothing consults one before allowing an action, so holding a capability isn't yet the\n\nsame as being permitted.\n\n## Signed jobs, because a loan outlives a conversation[¶](#signed-jobs-because-a-loan-outlives-a-conversation)\n\nA sponsored job needs one more property: it has to survive the participants going\n\noffline. If I fund work in a channel and your agent accepts a task on Tuesday and\n\nfinishes it on Thursday, the offer, the acceptance and the completion all have to\n\nremain verifiable after the sessions that produced them are gone — otherwise\n\n\"attributable per job\" quietly means \"attributable until someone reconnects\".\n\nThat's specified as a typed, addressed, signed, stateful action with a lifecycle\n\n(`offer → accept/decline → progress → complete/fail/cancel`\n\n) carried on IRCv3 client\n\ntags, written up as [RFC v0.4](https://github.com/freeq-irc/freeq/blob/main/docs/HANDOFF-RFC.md)\n\nwith zapnap. The signing primitives are built and tested in the SDK. The lifecycle (transition\n\nvalidation, materialised state, delivery addressed to an identity across servers) is\n\nnot, and neither is walking back through the log to verify an action\n\nsigned last week. That deserves its own post, and it's the piece that turns a\n\nsponsored task into a durable obligation.\n\n## Where this actually stands[¶](#where-this-actually-stands)\n\n| Status | |\n|---|---|\nIdentity rails: DID auth, creator-to-agent certificate, per-message signatures, keys kept by `(did, kid)` |\nWorking |\n| Metered model path, credential held server-side | Working |\n| Budget refuses before any upstream call | Working, verified by hit count |\n| Cost from the provider's token counts | Working |\n| Named sponsor, spend rollup, budget inheritance by children | Working |\n| Capability narrowing on delegation, operator grant and withdrawal | Working |\n| Consent before naming someone as sponsor | Not built |\nLending a lender's own capacity, not the operator's |\nNot built |\n| Capability checks gating operations | Not built |\nDurable signed jobs (`act` lifecycle, identity-addressed delivery) |\nSpecified, primitives in the SDK |\n| Verifying a historical action's signature | Not wired, though keys are kept |\n\n## The last mile, and whose pool it is[¶](#the-last-mile-and-whose-pool-it-is)\n\nSo: share the tokens, not the API key. freeq can now delegate bounded access to model\n\ncapacity without exposing the provider credential. The caller authenticates as its own\n\nidentity, the budget is checked before dispatch, usage is measured from the provider's\n\nresponse, and an exhausted budget stops the upstream call from happening.\n\nWhat it cannot yet prove is which durable, signed job authorised that particular call.\n\nThat is where the handoff work meets the metering path.\n\nThe capacity being shared today belongs to the server operator. There is one provider\n\ncredential and the server holds it. So this is real, revocable delegation of an\n\noperator-funded pool, and not yet a general mechanism for me to lend you capacity from\n\nmy own account.\n\nConcretely, on `irc.freeq.at`\n\nright now there is no pool at all. The endpoint\n\nauthenticates you, checks your channel membership, resolves your budget and enforces it,\n\nand then has nothing to spend, because no provider credential is configured. Enabling it\n\nis two flags for whoever runs a server. That ordering is deliberate: the part worth\n\ngetting right is who may spend and how much, and a credential is the easy half.\n\nMaking the pool personal needs three more pieces: a lender can register a credential or\n\nfunding source under their own control, naming that lender as a sponsor requires their\n\nsigned consent, and the resulting capability gates the model operation itself. Then\n\n\"your budget authorised this\" becomes \"your capacity paid for this\".\n\n*Code: github.com/freeq-irc/freeq. The unbuilt\nrows above are represented by ignored tests with the reasons attached:*\n\n`cargo test -p freeq-server --test agent_native -- --ignored`\n\n[← All posts](/blog/) · Discuss in [#freeq](https://irc.freeq.at)", "url": "https://wpnews.pro/news/share-the-tokens-not-the-api-key", "canonical_source": "https://freeq.at/blog/share-the-tokens-not-the-api-key/", "published_at": "2026-07-26 21:50:41+00:00", "updated_at": "2026-07-26 22:22:33.061964+00:00", "lang": "en", "topics": ["ai-agents", "ai-infrastructure", "ai-tools"], "entities": ["Block", "Buzz", "freeq", "FreeqWorld", "AT Protocol"], "alternates": {"html": "https://wpnews.pro/news/share-the-tokens-not-the-api-key", "markdown": "https://wpnews.pro/news/share-the-tokens-not-the-api-key.md", "text": "https://wpnews.pro/news/share-the-tokens-not-the-api-key.txt", "jsonld": "https://wpnews.pro/news/share-the-tokens-not-the-api-key.jsonld"}}