The network
Paying from an Arc wallet
No account, no dashboard, no human-held key. An agent with USDC on Arc buys its own compute, request by request.
Why this exists
An API key is a human artefact. Someone creates it, stores it, rotates it, and is on the hook for whatever spends it. That works badly for software meant to run unattended for months and pay for its own work.
Arc settles in USDC with sub-second finality and fees denominated in the same asset, which makes per-request payment from a wallet practical rather than theoretical. Vacuum exposes that directly: if the caller can sign for an address holding USDC, it can buy inference.
Open a channel
One on-chain transaction per request would cost more than most requests. Instead the agent funds a payment channel once, then spends it down with signed off-chain messages. The contract holds the balance; only the opening and the settlement touch the chain.
import { createWalletClient, http, parseUnits } from "viem";import { privateKeyToAccount } from "viem/accounts"; const arc = { id: 5042, name: "Arc", nativeCurrency: { name: "USDC", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } },}; const account = privateKeyToAccount(process.env.AGENT_KEY);const wallet = createWalletClient({ account, chain: arc, transport: http() }); // Fund a channel the agent can spend down without a transaction per request.const hash = await wallet.writeContract({ address: VACUUM_CHANNELS, abi: channelAbi, functionName: "open", value: parseUnits("5", 18), // 5 USDC; gas on Arc is USDC at 18 decimals});0x3600…0000 reports 6. Both are correct; use 18 for value and gas maths, 6 for token transfers. Mixing them is off by a factor of a trillion, and it is the most common mistake on this chain.Signing a request
The SDK handles the nonce, the signature and the channel accounting.
import { vacuum } from "@vacuum/sdk"; const vac = vacuum({ wallet }); const { text, receipt } = await vac.complete({ model: "qwen2.5-32b-instruct", prompt: "Draft the weekly summary.", maxTokens: 400,}); console.log(receipt.node, receipt.tokensOut, receipt.commitment.root);Underneath it is three headers on an otherwise ordinary request.
POST /v1/chat/completionsX-Vacuum-Channel: 0x7d1e...48abX-Vacuum-Nonce: 184X-Vacuum-Signature: 0x9c2f...0e71Content-Type: application/json { "model": "qwen2.5-32b-instruct", "messages": [ ... ], "max_tokens": 400 }| Field | Type | Description |
|---|---|---|
| X-Vacuum-Channel | address | The funded channel this request draws from. |
| X-Vacuum-Nonce | integer | Strictly increasing per channel. A reused nonce is rejected, which is what prevents replay. |
| X-Vacuum-Signature | bytes | The agent signing over the channel, the nonce and the request hash — including max_tokens, which bounds what this signature can authorise. |
There is no key to leak and nothing to revoke: the signature authorises exactly one request, up to a cost the agent set itself. Compromise of a single signed message costs one request.
Closing out
The router accumulates the agent’s signed spends and settles them against the channel on-chain, either periodically or when the agent closes it. Whatever is unspent returns to the wallet.
await vac.closeChannel(); // settles spends, returns the remainderA channel left open is not lost — it can always be closed by its owner after the dispute period, even if the router never does. That path exists so an agent’s funds never depend on our availability.
Keeping it bounded
An agent that can spend is an agent that can overspend. The constraints worth setting before it runs unattended:
- Fund the channel small. It is the only real cap. Top it up on a schedule rather than opening it large.
- Always set
max_tokens. It is what the signature commits to, so it is the per-request ceiling. - Use a dedicated address. Never the agent’s treasury. The signing key lives wherever the agent runs.
- Watch the receipts. Each call returns one; an agent that logs them can be audited later like any other spender. See Receipts and proofs.