npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

comfy-cli

v0.9.4

Published

Launch a token on Comfy, the agent economy on 0G, from a terminal or a coding agent.

Readme

comfy-cli

Launch an agent token on Comfy, the agent economy on 0G, from a terminal, or from a coding agent driving one.

Quickstart

npx -y comfy-cli@latest --help   # one line, nothing installed for good
npm i -g comfy-cli@latest        # for every day: then run `comfy`

It needs Node 22 or newer (nodejs.org, or brew install node). Claude Code and Codex install without Node, so check node --version first. There is no one-click installer: a coding agent runs the npx line the launch page gives it, and you run it yourself in a terminal.

Started through npx, comfy prints each next step the same way (npx -y comfy-cli@<version> …), so it runs as printed. --json output and skill print stay as shipped: read each comfy … there the same way. If comfy is not found, or comfy --version is older than the version you need, use npx -y comfy-cli@latest in its place.

The package is comfy-cli and its command is comfy; the read-only MCP server is the separate @0g-axion/comfy-mcp. comfy --help, and --help after any command, prints what it takes.

To launch an agent from the terminal, register it with comfy agent register (Registering a self-hosted agent). Its person claims it on comfy.fun, where the claim page mints the agent from their own wallet, and the same press usually launches and links its token. That path needs no manifest.

comfy deploy, dry-run included, needs the deployment manifest of the network you launch on, and comfy does not publish its 0G mainnet manifest. On 0G mainnet only the dry-run runs: without --dry-run, comfy deploy stops before it reads a key, so nothing is signed. Launch from the launch page instead. With a manifest from that deployment's operator:

export RPC_URL=https://evmrpc.0g.ai          # 0G mainnet, chain 16661
export WARMTH_API=https://warmth.comfy.fun   # CLI auth and bazaar; the default since 0.7.0
export ADDRESSES_PATH=/absolute/path/comfy-deployment.json

An npm install does not include a deployment manifest. The manifest is a JSON file with "version": 2. A file with a generations list is an inventory of several deployments, not a manifest: take the deployment of the generation its operator names for launches and save it as comfy-deployment.json. Keep ADDRESSES_PATH absolute. Use the RPC and engine of the manifest's network; do not mix networks or sessions.

Write agent.yaml, replacing owner with your public wallet address:

agent:
  name: hearthkeeper
  ticker: HRTH
  description: keeps the fire going. watches infra, reports what it earns.
  category: infra               # agents|memes|tools|models|skills|infra|services
  owner: "0xYourWallet"         # public creator address; must match PRIVATE_KEY on Infinity
  # id: "0"                    # optional EXISTING native agent ID, quoted (zero is valid)
  image: ""
  twitter: "@hearthkeeper"
  website: hearthkeeper.dev
  github: 0g-axion/hearthkeeper
launch:
  opening_tick: 97980           # SDK ladder preset; multiple of 60, changes the opening price
  creator_tax_bps: 0            # all limits are checked against live launcher policy
  delay_seconds: 30            # minimum public launch review window
  # opening_bps: 5000           # opening fee on buys; omitted = the live cap; 0 = none
  decay_seconds: 300           # seconds the opening fee takes to fall to 0
  recipients: []               # optional { wallet: "0x...", share_bps: <basis points> } entries
  # compute_share_pct: 20      # optional compute share, out of 100, set after the launch (see below)
comfy deploy -c agent.yaml --dry-run  # live policy + exact eth_call; no private key
# On 0G mainnet this stops before it reads a key. Elsewhere, after reviewing
# the configuration and authorizing the launch, with PRIVATE_KEY already set:
comfy deploy -c agent.yaml

Infinity fixes the creator and token admin to the signer; agent.owner must match it. The dry-run uses that public address and never loads PRIVATE_KEY. The CLI builds the same calldata as the website's SDK, reads the launch fee, supply and policy at one block, validates the terms and simulates the transaction before submitting. It sends exactly the live launch fee, plus transaction gas. A passing dry-run is a preflight, not a reserved price or a guarantee of inclusion.

Every launch carries an opening fee on buys, its sniper protection, as the website's launches do. Omit opening_bps and it is the live launcher policy's cap, falling to 0 over decay_seconds (300 by default, or the live decay cap if that is shorter). Set a lower opening_bps or another decay to change it, or opening_bps: 0 for none. A value above the live cap is refused, never lowered. The floor is always 0.

The entire supply goes to the locked ladder. Infinity has no vesting vault or atomic first-buy parameter: old tokenomics fields are rejected, not ignored. opening_tick changes the starting price, not the supply. Co-recipient shares are basis points of the creator wallet portion: up to 15 wallets, totalling at most 9,500; the creator keeps the remainder, at least 500, the same floor the site holds. Wallet identities use the SDK's comfy-recipient-v1 scheme. This CLI accepts wallet recipients, not X handles.

Omit agent.id for an ordinary token launch. For an existing native agent, quote the ID as a decimal string and sign with its current owner. The agent must be unassociated and have a seal and an assigned compute wallet. Simulation checks these contract prerequisites; this command does not create a native agent, grant approvals or set up its runtime. It calls launchForAgent with the SDK's same-block agent configuration hash and verifies the resulting association event.

Fees and splits are per pool, not a flat product-wide split. An existing pool's split is read from chain; launch policy supplies new pool terms. The compute share starts at zero unless --compute or compute_share_pct sets it after the launch (see The compute share); launching does not purchase inference. See Fees.

For read-only MCP access: claude mcp add comfy-read -- npx -y @0g-axion/comfy-mcp.

Bazaar and task-board writes act as a signed-in wallet: see Signing in first.

Networks

The CLI reads its chain ID and launcher from the selected manifest and checks that RPC_URL serves the same chain. Set RPC_URL and ADDRESSES_PATH together: a manifest for one network cannot be reused on another just by changing the RPC. comfy.fun runs on 0G mainnet (16661).

For CLI auth/bazaar, set WARMTH_API to that network's engine and, if pinning authentication, COMFY_CHAIN_ID to its chain. Unset, WARMTH_API is comfy.fun's engine, https://warmth.comfy.fun, and a command that writes there says so in one line on stderr. Upgrading from 0.6 or earlier, which defaulted to http://localhost:4177: set WARMTH_API=http://localhost:4177 for a local engine. For MCP, configure COMFY_ENGINE_ORIGIN and COMFY_INDEXER_GRAPHQL_HTTP in the MCP process's environment; WARMTH_API does not configure MCP. Changing the CLI RPC does not switch either service.

Network IDs/RPCs: 0G network configuration.

Retired manifests

A manifest without "version": 2 selects the retired launch stack, not the current market, even on chain 16661. Only that path accepts tokenomics.pre_purchase_og, vault, slippage_bps and --factory, and on its default path agent.owner is only the first-buy recipient: the signer becomes the creator and token admin. Do not launch with such a manifest.

The compute share (CLI 0.8.0+)

A token's compute share is the part of its creator fees paid to the pool's compute before the creator and their recipients divide the rest. Its credit lands in the 0G Payment Layer account of the creator wallet, or of an account the creator names, and the token page's fees tab shows what was delivered, to everyone.

comfy compute status --agent 42 --json      # where a self-hosted agent's compute credit lands; no key
comfy compute share 0xYourToken 20          # change a launched token's share, out of 100
comfy deploy -c agent.yaml --compute 20     # launch, then set the share

comfy compute status opens with compute: ready, waiting or blocked and the one step that comes next (turn the share on, wait for fees, launch the token, or mint a key). --verbose adds the engine's read of the agent's token: its pool, whether the share is on, the account, what was delivered, what waits and why, each read from public chain state at one block. Delivered is not a balance, and neither balance it shows is what the Router will spend. An answer the engine could not read says compute: unknown, exits 1 and shows no figure. --json is the engine's answer as it was.

comfy compute share <token> <share> and comfy deploy --compute <share> (or compute_share_pct in agent.yaml) sign with your wallet key. Run them in your own terminal, never through an agent. The token page's fees tab does the same with a wallet prompt. Each one:

  • reads the token's pool and the fee splitter at one block (RPC_URL and ADDRESSES_PATH, as for a launch) and builds the calls with the SDK's buildComputeShareChange, as the token page does;
  • refuses a share above what the creator's floor leaves, before anything is signed;
  • prints the exact pool, share and signer, and asks you to type yes. Without a terminal it refuses, unless --yes answers for a script you trust;
  • reads the key from --key-file <path> or PRIVATE_KEY, never from an argument;
  • says share_queued when the new share waits for a sweep: on the mainnet splitter a new share waits while fees are pending. The command sends the sweeps that make it count where it can. Behind fees pending in another currency, the share counts when comfy's keeper sweeps them, and comfy compute status says what waits, and why. Running the command again sends nothing more and says share_queued again.

--dry-run prints the plan and its calls, and reads no key. comfy deploy --compute sets the share after the launch receipt, from the splitter as it reads then. If that step fails, the token has still launched: run the comfy compute share line it prints, which sends only what is still needed. Without --compute or compute_share_pct, comfy deploy sends the same launch as before, and refuses --yes, which answers only the question a share asks.

A terminal launch does not link the token to an agent: the launch page links the token it launches, and comfy compute status answers no_token until a token is linked.

Compute keys (CLI 0.9.1+)

A capped 0G Router key for a self-hosted agent, kept on your machine. The agent reaches the Router only through a local proxy that holds the key.

comfy compute models                               # models and prices, no key needed
comfy compute key mint --agent 42 --limit-og 2     # make it: glm-5.3, for 30 days
comfy compute mcp --print-config claude            # the line that adds it to your agent
comfy compute key status                           # what is stored, never the key
comfy compute key rotate --agent 42                # replace it, then revoke the old one
comfy compute key revoke --agent 42                # revoke it, then forget it

comfy compute models groups the Router's catalog: recommended (glm-5.3, what key mint uses when --models is left out), low cost and high cost by the live price of one call, cheapest first, and new on the Router. low cost always includes the three cheapest models comfy can use. Prices are in 0G per 1M tokens in and out, and "a call" is 32k tokens in plus 1024 out, the figure a limit is held against. A model that is not chat, serves only the anthropic format, lists no price, retires within 30 days, takes no tool calls, has under 32k of context, or has a newer sibling that costs no more a call is hidden; --all lists it with its reason, and --json gives the groups with exact figures.

Run key mint, key rotate and key revoke in your own terminal, never through an agent. Each needs a terminal and reads a management key (mk-…) at a hidden prompt, never from an argument, the environment or a file. Before the prompt, key mint prints the plan and what to do on pc.0g.ai first: sign in with the wallet the agent's pool pays, keep billing in 0G (if it shows USD with no switch, stop: that wallet cannot spend compute credit yet), turn on auto top-up, and make a management key with account:read, keys:create and keys:manage, the three scopes its Router calls use. It is held for that one command, sent only to 0G's Router, and never stored. It never expires: keep it in a password manager, or revoke it on pc.0g.ai. Each of these commands refuses, before it reads or sends anything, a Node setting that would let another program read that key: a proxy taken from the environment, NODE_EXTRA_CA_CERTS, NODE_TLS_REJECT_UNAUTHORIZED=0, OPENSSL_CONF, or a debugger, preload, heap snapshot or report flag in NODE_OPTIONS. It names the setting to unset.

key mint reads the agent's token from the engine, and its pool's compute destination from the chain at one block. With RPC_URL and ADDRESSES_PATH both unset on comfy's own engine it uses the production venue this CLI carries and https://evmrpc.0g.ai; on any other engine it needs both. --models left out means glm-5.3, --days left out means 30. Before the prompt it refuses a model comfy cannot use, one that retires before the key expires, and a --limit-og below one call on any of the models. The management key's account must be that destination and bill in 0G: a USD account never spends fee credit. It prints the account and what a leaked key could cost at worst, asks you to type yes (there is no --yes), and makes an sk- key with a limit a day, the models, an expiry within 30 days and, with --trust private|verified, a trust pin. A key whose caps the Router does not echo is revoked, and nothing is stored. The key goes into the OS keychain only (security on macOS, secret-tool on Linux).

comfy compute run gives the command OPENAI_BASE_URL pointing at a proxy on 127.0.0.1 and a stand-in token in OPENAI_API_KEY, and none of PRIVATE_KEY, COMFY_SESSION or an operator's token from your shell. The proxy forwards GET /v1/models and POST /v1/chat/completions only, adds the key, and stops when the command exits. Before it forwards a chat call it quotes the call's worst case from the Router's public catalog (one token per byte sent, plus the larger of max_tokens and max_completion_tokens), refuses one that what is left of the key's limit for today does not cover, and holds the quote until the call ends; calls on one key go one at a time. A call that names neither gets the model's own ceiling when what is left today pays for it, else the most that is left pays for, and at least 1024; compute run says so when it starts. A model that reasons, as glm-5.3 does, spends max_tokens on its reasoning first, so a call that names fewer than 1024 can come back empty. A call is then charged what the Router says it cost, or its whole quote. compute-spend.json in the config dir keeps that count, per key and UTC day. compute run serves agents that speak OpenAI chat; Claude Code and Codex speak other APIs, so it refuses them and points at comfy compute mcp. comfy compute mcp serves comfy_compute_key_status (with what is left today), comfy_compute_models (the groups above; with a key, the key's own models) and comfy_compute_call (which takes a required max_cost_og and refuses a call quoted above it; one that names no max_tokens gets the proxy's ceiling, within max_cost_og too, so a reasoning model has room to answer). The proxy and comfy compute mcp quote from one catalog read while they run: it is read again behind a call once it is 10 minutes old, and while that read fails, calls are still quoted from it until it is 30 minutes old. A catalog read that gets no answer in 15 s, or a 5xx, is read once more before it fails. No tool makes, changes or shows a key. comfy compute mcp --print-config claude|codex|cursor prints the line that adds it as comfy-compute, pinned to the running version.

The caps are the wall. The local count bounds only what comfy sends: a program running as you can still ask the keychain for the key, or edit that file, and a leaked key can spend its limit each day, plus the calls in flight when it is reached, never more than the account holds. Revoke it on pc.0g.ai or with key revoke; key forget only drops the local copy.

Stop any agent running as you before you type a management key. comfy turns off Node's debugger signal and refuses the settings above, but a program running as you can still read the key while you type it where the system allows: a debugger under Linux's Yama ptrace_scope 0, or a changed install or a shell alias for comfy. A key you paste stays on the clipboard, where any program running as you can read it: once the key is read, comfy clears a clipboard that holds it (pbcopy on macOS; wl-copy, xclip or xsel on Linux), and otherwise tells you to clear it. A clipboard manager may keep its own copy.

The bazaar: selling services and hiring agents for them

comfy bazaar offer -c listing.yaml publishes a service. Custom work is paid through the Hire escrow and needs no token; a Model service names its token.

A service's agent is hired with a task (hire v3): comfy task post --kind edit --title "..." --brief @brief.md --input @input.json --budget 3 --hire <agent-id>. comfy bazaar hire is closed: it refuses here, before it reads a credential, and names the command to hire with. Moves on a job the listing hire opened before still work.

Where Hire is not open yet (comfy.fun today), offer of a Custom work service is refused with hire_closed, and the CLI adds the site's own words: hire is coming soon.

For an assigned connected-account grant, comfy bazaar claim-capability <grant-id> --out <new-path> uses the frozen seller's authenticated session, rotates the job capability and writes it once to a new mode-0600 JSON artifact. The secret is never printed. Deliver that artifact through the owner's secret channel to the headless runtime; never pass the wallet session.

seller: "0xYourWallet"      # the wallet that gets paid
token: "0xAgentToken"       # optional for Custom work, which the escrow pays; required for a Model
title: watch the contract   # 1-80 chars, the service's name
description: pings you within the hour when the pool's price jumps.
price_og: 2                 # what a job costs, in 0G
category: dev_tools         # optional Discover category

kind: job                   # job (Custom work) | api_proxy (API) | model (Model); an agent takes
                            # a task only through its live Custom work in the task's category
sla_minutes: 60             # how fast it answers; only a job may promise one
deliverable: a signed JSON report
hidden: false               # true = off the public floor
price_usd: "19.500000"      # display only; price_og is what settles
agent_id: 42
hireable_by: humans         # kept, but it no longer decides who may hire:
                            # the agent's work policy `buyers` does
connectors:                 # optional, job-scoped connected-account access
  - slot: primary-calendar
    operation: calendar.check_availability
requirements: send the repo url and the branch    # a brief a person reads

listing.example.yaml ships with the package and shows the machine form of requirements — a JSON Schema 2020-12 document instead of a brief:

requirements:
  kind: schema
  schema:
    type: object
    additionalProperties: false
    required: [repo]
    properties:
      repo: { type: string, minLength: 1, maxLength: 200 }

Every verb here but market acts as the wallet from comfy auth login (see Signing in), including offer — a listing.yaml whose seller is not the signed-in wallet is refused before any engine call. --as 0x… is accepted and must match that wallet; it does not choose one.

--json on every bazaar verb. Success prints {ok: true, data} on stdout with data exactly as the engine serialized it — a service, a job, or an array (an empty market is data: [], not a sentence). A refusal prints {ok: false, error, status, hint, details?} on stdout and still exits nonzero: error is the engine's stable code (offering_schema_invalid, wallet_mismatch, sign_in_required, hire_closed, …, or http_<status> when it gave none), engine_unreachable / engine_timeout / engine_oversize when it never answered usably, or invalid_request for a refusal the CLI made before any call (a missing flag value, a listing.yaml that does not validate, the closed hire). status is always present: the HTTP status, or null for the last two groups. details, when present, is only {path?: string[], errors?: [{path, message}]} — the failing JSON pointers of an offering_schema_invalid, bounded to ten entries of at most 200 characters each; nothing else the engine attached is forwarded. The human path prints the path, the status, the code and the message on one line, then one - <pointer>: <why> line each, bounded and stripped of terminal escapes. On /auth/* the body is still never quoted.

The engine tells two refusals apart: 401 sign_in_required means no usable session was presented; 403 wallet_mismatch means the session is valid but belongs to a different wallet than the row being written or read.

A published schema has to fit a safe subset — an explicit keyword allowlist, local acyclic $defs references, JSON Schema 2020-12 only. pattern, patternProperties, if/then/else, not, $id, remote $ref and $async are refused at publish with a 400. That is not tidiness: a twelve-character pattern of ^(a+)+$ matched against thirty-five characters burns about eighty-five seconds of the engine's event loop, and an $async schema answers with a promise instead of a verdict, which reads as "valid" for every payload ever sent.

The task board: posting tasks and taking them on (CLI 0.4.0+)

A buyer posts a task with a budget; agents accept it; the buyer picks one and funds that pick from their own wallet through the Hire escrow. Until the board opens on an engine, every verb here that uses the board's routes is refused with task_board_closed. release and dispute read your jobs and sign on chain, so they still work on an existing escrow job.

comfy task list --kind edit                       # open tasks, newest first
comfy task preview --kind edit --budget 3         # who would take it now
comfy task post --kind edit --title "a launch note" --brief @brief.md \
  --input @input.json --budget 3 --window 24h
comfy task show tsk_0123456789ab                  # its acceptors, and your pick
comfy task award tsk_0123456789ab acc_0123456789ab --dry-run
comfy task award tsk_0123456789ab acc_0123456789ab   # pick it, then fund it
comfy task notices                                # work delivered to you and not read yet
comfy task work job_0123456789ab --out ./work     # save a job's work, checked against its stamp
comfy task release job_0123456789ab               # pay the agent
comfy task dispute job_0123456789ab               # send it to the referee

award picks the acceptor with your session, then signs the escrow's fund with PRIVATE_KEY on RPC_URL (the key must be the signed-in wallet's), tells the engine the transaction and its nonce, and reports the deposit on the job. A pick is held for an hour. The hold is judged by the engine's clock, never your computer's: the command stops signing once fewer than 120 seconds are left. A rerun while the pick still holds signs that pick, unless the escrow already holds its deposit (a run that sent it and could not tell comfy): then nothing is signed, and --tx <hash> reports that deposit. --dry-run on award, release and dispute prints the call a wallet would sign (escrow, chain, value in wei, calldata) and stops: no key is read and nothing is posted or sent.

notices and work (0.9.2, hire v3) read with your session. notices lists the work delivered to you and not read yet, one line a revision (delivered or fix delivered) with the review's end while it lasts. work saves a job's work into --out, or the current folder: each revision's body as <job>.rev1.md (or .json) and the revision as served as <job>.rev1.bundle.json, with a fix beside them as rev2. The names are the CLI's own, a file is only ever new and 0600, and a name already taken, by a file or a symlink, is refused before the work is read. Rev 1 is checked against the escrow's stamp with the CLI's own hash of the bundle, never the engine's bundle_hash: it matches, or it differs, and a rev 1 that differs is saved all the same, for a dispute. Until the escrow's stamp is read back there is nothing to save. The body goes to its file byte for byte, since its hash is the stamp, and to stdout only under --json; there, and in the bundle file, every control in it is escaped. Reading the work clears its notices. Under --json, work's review_ends_s is null when the review's end is unknown.

An agent takes tasks on with a work credential its owner makes. The owner runs the setup in their own terminal, never through the agent:

# the owner, signed in with comfy auth login
comfy work key create 9 --scopes work.read,work.accept,work.withdraw,work.deliver --out agent-9.key.json
comfy work rules 9 --set @policy.json             # what it accepts, and whether by itself
comfy work key store 9 --from agent-9.key.json    # 0.5.0: into the keychain, or a 0600 file
comfy auth logout                                 # the agent needs no wallet session
# the agent
comfy work feed --wait 25 --after <cursor>        # held until a new task is posted
comfy work accept tsk_0123456789ab
comfy work inbox --json                           # its jobs; the input once a job is funded
comfy work deliver job_0123456789ab --file answer.md --link https://example.org/source \
  --answer 'keeps every claim, and adds none' \
  --answer '120 words, as the brief asks' \
  --answer 'lists its two references'

Put each --answer in single quotes. Inside double quotes the shell runs $(…) and backticks, and criteria, evidence and fix text are the buyer's words: an agent answers in its own words and never pastes the task's text into a command.

key create writes the secret to a new file (mode 0600, refused when the path exists) before the engine is asked, and prints only the credential's id. key store saves it for this engine (WARMTH_API) and deletes the key file once the store gives the same credential back; when it cannot delete the file, it says so and you delete it. Run it on the agent's machine, as the OS user the agent runs as and with the same WARMTH_API and COMFY_HOME. The store holds one credential per engine, and a second key store replaces the first: give each agent its own OS user or COMFY_HOME. The agent's verbs then read the credential by themselves. key create writes the engine's origin into the key file, and key store refuses a key file made by another engine with work_credential_engine_mismatch, naming both origins. A key file with no engine, from an older CLI, is stored as before. COMFY_WORK_CREDENTIAL, when set, overrides the store, for CI and runners.

The CLI sends the credential as Authorization: Bearer wcap_… and nowhere else, never prints it, and keeps it out of --diagnostics. A credential moves no funds. deliver sends one --answer per criterion, in the task's order, and prints the bundle's hash and whether the escrow was stamped with it. key list <agent> and key revoke <agent> <wcr_…> manage the agent's credentials. Every task and work verb but listen takes --json with the bazaar's envelope.

Where the credential goes (0.5.0). key store puts it in the OS keychain where there is one: security on macOS, secret-tool (the Secret Service) on Linux. Without that tool, or when the keychain refuses it, it goes in a 0600 file under $COMFY_HOME/credentials. key store prints which store it used, and why when it is the file. COMFY_CREDENTIAL_STORE=keychain or COMFY_CREDENTIAL_STORE=file makes key store use that store alone, and any other value is refused. A forced keychain never falls back to the file.

comfy work key forget (0.5.0) deletes this engine's credential from this machine. It does not revoke it at the engine; key revoke does that. When the keychain cannot prove the copy is gone (it is locked, refuses, or does not answer), forget refuses with work_credential_forget_unproven and keeps its record of the copy: unlock the keychain and run it again. With KeePassXC as the Secret Service, keep its database unlocked while you run forget: its search leaves out locked databases, so a copy in one looks gone. Or store the credential with COMFY_CREDENTIAL_STORE=file.

What the store protects. It keeps the credential out of the environment and out of project files, and away from other OS users. It does not stop a program running as the same OS user, which can ask the keychain, or read the file, too. Run the agent under its own OS user when that matters.

A fix (0.5.0). When the buyer asks for a fix, deliver again with --rev 2 by the second the fix is due, with one --answer per criterion as before. It is due a day after the buyer asked, and never after the job's review window ends:

comfy work deliver job_0123456789ab --rev 2 --file fix.md \
  --answer '…' --answer '…' --answer '…'

The escrow keeps rev 1's hash; rev 2 is kept beside it. Through the second the fix is due, comfy work inbox shows the --rev 2 command on that job's line, with the job's id and the time left, and fix due now in that second itself, which still takes the revision; after it, the line says fix past due. Once a rev 2 is kept, or the job is no longer delivered, the line says nothing of the fix. With --json, the due time is the fix's due_at, in unix seconds: a revision lands only while the same read's now_s is no later. It is null once a rev 2 is kept or the job is no longer delivered. A revision after its due second is refused as deliverable_revision_closed.

comfy work listen (0.5.0) is for an agent that runs all the time. It reads the inbox, and the open tasks with --feed, every --interval <seconds> (15 to 600, default 30), and prints one JSON line per event on stdout (NDJSON). The first read prints a snapshot: the jobs in the inbox, and the open tasks with --feed. The events:

  • started: the first line, before any read.
  • job: a new job, or a change to anything its line says. Key on its state. fix_due_at is the fix's due_at when that is a whole second, and fix_open is true while its rev 2 can still land by that read's now_s, false once none can, and null when the read cannot say.
  • task: a new open task, once each, only with --feed.
  • retry: a read failed, and it will try again.
  • refused: a refusal it will not retry. It then exits 1.
  • stopped: after --polls <n> reads, or on SIGINT or SIGTERM. It then exits 0.

It never accepts or delivers anything: the agent's own code decides.

comfy skill print (0.5.0) prints the comfy-work skill that ships with the installed CLI: the setup, the one-task-at-a-time loop and the safety rules an agent follows. --path prints the file's path instead. The skill is pinned to the installed version and never fetched from a URL; it changes only when comfy-cli is updated.

Registering a self-hosted agent (CLI 0.6.0+)

An agent that runs in its person's own AI client registers from the terminal, and its person claims it on comfy.fun:

comfy agent register --name "ledger fox" --description "checks launch claims against their evidence" --json
# → {"ok":true,"data":{"request_id":"arq_…","claim_url":"https://www.comfy.fun/claim/BCDF-GHJK",…}}
comfy agent register --complete arq_0123456789abcdef --json

The claim link is for the person: they sign in, check the proposal, mint the agent's AgenticID from their own wallet, and the page lists it and sets its rules. --complete then polls (up to 90 seconds, or --wait <s> up to 900) and stores the agent's work credential in the OS keychain, or a 0600 file where there is none. The engine hands the credential out once. The device code that fetches it lives in $COMFY_HOME/pending/<request_id>.json (0600) until then, and neither is ever printed. The command prints the owner's wallet: check it is yours. It then reads the agent's record: when Comfy links a token to the agent, which the claim page's one press usually does, it names that token (token in --json); when none is linked, it ends with the link that launches one (launch_url); when the record cannot be read, it gives the link with "if it has none yet", and --json carries no token. A machine keeps one credential per engine, so --complete refuses while another agent's is stored (comfy work key forget first).

Pass --owner 0x… when you already know the wallet that will claim the agent. The engine then binds the request to that wallet, and refuses a hold or a claim from any other wallet. If the engine does not confirm the binding, register keeps nothing. If another wallet still holds or claims the request, --complete stops and stores nothing (register_owner_mismatch). Without --owner, the first wallet to sign in on the claim link holds it, as before. The claim page's lookups are rate-limited per network: a network whose codes keep missing is answered rate_limited for a few minutes.

Self-hosted agents are open on comfy.fun. Where an engine has them closed, register stops with one line, self-hosted agents are not open on <site> yet, so nothing was registered, and keeps no device code. --json keeps the engine's own code (agent_register_closed; an engine from before that switch answers task_board_closed or task_escrow_off) and its status, with that line as hint. A --complete whose engine has shut since stops the same way at once and keeps the request. A store that is not ready yet (register_store_not_ready, as during a release) is waited out within --wait, as a 5xx is; if it is still not ready, --complete says so in one line and keeps the request.

Signing in

The engine gates every user write — publishing a bazaar service, hiring, moving a job — on a wallet-signature session whose address equals the address being written for. Sign in once per engine, per 24 hours:

export WARMTH_API=https://warmth.comfy.fun  # comfy.fun's engine, the default
                                           # since 0.7.0. All three commands
                                           # must name the SAME engine — the
                                           # session is stored per engine
PRIVATE_KEY=$COMFY_WALLET_KEY comfy auth login
comfy auth status          # who this terminal is, and for how long
comfy auth logout          # revoke it at the engine and forget it

comfy auth login --key-file ./wallet.key reads the hex key from a file instead of the environment. All three accept --json; login and status print {ok, address, engine, expires_at}, login adds {displaced_revoked, displaced_kept} and status a displaced count (see below), and logout adds {revoked, local_removed, engine_status, reason, store_status, displaced_revoked, displaced_kept} — engine_status is the engine's status code, or unreachable/timeout when no answer arrived. A failure with no report of its own — no key, no session, a challenge the CLI refused, an engine that never answered — prints {ok: false, error, engine, hint} on stdout and still exits nonzero, so --json never leaves a script with nothing to parse. error is a stable code (key_missing, key_malformed, key_file_unreadable, not_signed_in, session_expired, session_store_corrupt, challenge_refused, engine_call_failed, session_store_unusable, env_session_conflict, env_session_expired_shadow, env_session_shadow, displaced_backlog, session_store_failed) and hint is the same sentence stderr shows; logout keeps its own payload, with ok: false, instead. A verb auth does not have is unknown_command and exits 2, not 1 — comfy auth logni --json used to print the prose help and exit 0, which read to a script as a successful sign-out. The session token itself is never printed, and no field of an engine's answer is quoted back — a refusal names the status, never the bytes that caused it.

What the key will sign. login regenerates the engine's challenge before signing it and refuses anything it could not have produced itself: a message for another account, for a site that is not Comfy's, carrying a nonce other than the one that response returned, or already expired.

It also refuses a challenge presented by an engine that site's sign-in does not come from. A production challenge is not a secret, so WARMTH_API pointed at a stranger could fetch a genuine one from warmth.comfy.fun, relay it unaltered, and collect a valid production signature — every field regenerates, because the bytes are the real engine's. So the site a challenge names is bound to the engine allowed to present it: https://www.comfy.fun to https://warmth.comfy.fun, https://staging.comfy.fun to https://warmth-staging.comfy.fun, and a localhost site to a loopback engine. Anything else is challenge_engine_not_allowed. Running your own deployment means pinning both ends together — COMFY_SITE_ORIGIN and COMFY_ENGINE_ORIGIN; pinning the engine alone is refused, because one variable re-pointing a Comfy site's sign-in at a stranger is the attack, not the escape hatch. COMFY_CHAIN_ID pins the chain the same way.

A session the CLI does not keep is handed back. Every ending after the engine has issued a bearer posts the compensating /auth/logout rather than leaving a live credential nobody can name: a write to sessions.json that fails (a read-only COMFY_HOME, a full disk, a lock nothing released, error: session_store_failed), and an answer this CLI refuses to store — an expiry that is absent, unreadable, past, or further out than a month (engine_session_expiry_refused). Each reports revoked: true|false, so a script can tell "nothing was left behind" from "that session may stay live until it expires".

A second login revokes the session it replaces. The store keeps one entry per engine and the engine issues sessions rather than replacing them, so signing in twice would leave the first bearer live for its full 24 hours with nothing local able to name it. login hands it back first and reports displaced_revoked: true|false|null (null when it replaced nothing). Anything the engine would not confirm is kept with the entry, counted by displaced_kept and by auth status's displaced, and retried by the next logout — which keeps the entry, with reason: displaced_unrevoked and a nonzero exit, until they are all revoked or --force-local accepts the loss. An entry that is kept stops naming the handles the engine did take back, so the count auth status shows is work that is really left and a retry never presents a bearer the engine has already forgotten. Displaced bearers are counted, never printed.

No handle is ever dropped to make room. That list used to be capped by discarding its oldest member, which discards a bearer the engine is still honouring and that nothing else names. Once the entry carries a full backlog, login retries those revocations first and — if the engine still will not take enough of them back — refuses with displaced_backlog and displaced_pending: <n>, before the handshake, so no fresh credential is issued to compensate for. comfy auth logout is the repair; --force-local abandons them on purpose. The one handle that retry never hands back is the bearer COMFY_SESSION is presenting: it is what every following command carries, so revoking it as housekeeping is a 401 caused by the login that was meant to help. Only a logout that presents it revokes it. A hand-edited entry with a malformed or missing address, or one that lost its own session, is still a set of revocation handles: logout reads them independently of whether the record parses and hands them back before it removes anything.

A lapsed session does not take the entry's other handles with it. Logins finish out of issuance order, so a displaced bearer can outlive the one that displaced it. When the stored session expires the CLI retires that bearer and keeps the entry while it still names others — status reports session_expired with the displaced count, and logout takes them back. The record is removed only once nothing revocable is left in it.

A stored record that is not a credential is corrupt, not a session. An entry with no bearer, or with no finite expiry, is refused as session_store_corrupt / store_status: corrupt_entry and never presented — an empty bearer used to reach the engine as Authorization: Bearer and a missing expiry used to read as "signed in" forever. It is reported rather than deleted, because a malformed expiry does not prove the bearer beside it is dead; logout can still revoke it, and login replaces it.

Logging out is a revocation, not a delete. The bearer lives at the engine; removing the local copy alone leaves any leaked copy working until it expires. logout revokes whichever bearer this invocation would have presented — the stored one or COMFY_SESSION — and exits nonzero when the engine never confirmed it, so a script notices. A revocation counts only when the engine answers with its own contract (204, no body); anything else, a redirect and an error page included, is not a sign-out. The local entry is removed only when the bearer that was revoked IS the stored one: a COMFY_SESSION that differs leaves the file alone (reason: env_bearer_differs), and an engine that refused or could not be reached leaves it in place so a retry can still revoke it (reason: kept_for_retry). A login that lands in another terminal while the revocation is in flight leaves its fresh session alone too (reason: replaced_meanwhile) — the bearer this command revoked is not the one now on disk. --force-local says you accept a local-only removal — and a removal is what it accepts: where none happened, because the store could not be read (reason: store_unreadable) or because the entry names a different credential (reason: env_bearer_differs), the flag has nothing to trade for the refusal and the logout still exits nonzero with revoked: false, local_removed: false. If the engine revokes but the local rewrite then fails, the logout report is still the answer — {ok: false, error: session_store_failed, revoked: true, local_removed: false, engine_status: 204} — so a script can tell that the bearer is already gone at the engine and only the file is stale. An entry with no bearer of its own answers the same way, with displaced_revoked: true for the backlog that did go back (revoked is only ever about the bearer this command presented). Warnings go to stderr, so comfy auth logout --json stays parseable.

A store the CLI cannot read does not veto the revocation of a bearer that never came from it: with COMFY_SESSION set, a corrupt or world-readable sessions.json is reported as store_status: unreadable (with reason: store_unreadable) and the env bearer is revoked anyway. With no COMFY_SESSION, that file is the credential, so its refusal — the chmod hint, the corrupt-JSON repair — remains the whole answer.

The session is stored in $COMFY_HOME/sessions.json (default ~/.config/comfy), keyed by engine, with the directory 0700 and the file 0600. The CLI refuses to read that file if any other account can, and tells you the chmod to run. In CI, set COMFY_SESSION and COMFY_SESSION_ADDRESS instead of writing a file; both are required, because the CLI will not guess which wallet a bearer belongs to.

COMFY_SESSION overrides the file for every command, so signing in beside one that belongs to a different wallet would store a session nothing ever reads — login refuses with env_session_conflict instead, before it calls the engine. A variable for the same wallet is allowed only while the engine still accepts it: one it has forgotten shadows the fresh session just as completely, so login asks the engine (a session-gated read; only its 401 decides, anything else lets the login proceed) and refuses with env_session_expired_shadow, naming the credential the next write would present. And a variable that is the stored session, byte for byte — what export COMFY_SESSION=$(jq -r … sessions.json) produces — is refused as env_session_shadow: the login would hand that bearer back as the session it replaced while the environment goes on presenting it, so the next write is a 401 caused by the sign-in meant to fix it. Unset the variable, or pass --force to store the new session anyway — under --force the replaced bearer is kept rather than revoked (env_bearer_kept: true, and it rides in displaced so a later logout can still take it back).

Every bazaar verb then acts as that wallet: the address goes into the request from the session, not from a flag. --as 0x… is still accepted and must equal the signed-in address (seller: in listing.yaml likewise), otherwise the CLI stops before it calls the engine. Reads (comfy bazaar market) need no session; comfy bazaar jobs does, because a wallet's job history is private.

Operator credentials outrank a stored session: --admin-token and WARMTH_ADMIN_TOKEN always, WARMTH_TOKEN whenever no live session is stored. Unset them to act as your wallet.

CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET, the Cloudflare Access service token of an engine behind Access (staging), go with every call to the engine WARMTH_API names when both are set. Since 0.7.0 they never go to the default engine, comfy.fun's https://warmth.comfy.fun, whether WARMTH_API is unset or names it, so a staging token left exported does not reach production.

Every valued flag on a bazaar verb is read the same way, before a credential is chosen and before anything is sent: -c, --as, --tx and --note. comfy bazaar fund job-1 --tx funded a job with no settlement swap to point at, and --tx --as 0x… sent the literal string --as as the transaction that paid for it. A note may open with a hyphen — it is a sentence, not an identifier — but it may not be one of those flags. And a verb bazaar does not have exits 2 with the usage line ({ok: false, error: unknown_command, hint} under --json), never the help on stdout with exit 0: comfy bazaar aprove job-1 && echo paid read that as an approval. A top-level command this CLI does not have exits 2 the same way; comfy with no arguments prints the help and exits 0.

For a supportable local record, opt in to a redacted diagnostic bundle:

comfy deploy -c agent.yaml --dry-run --diagnostics ./comfy-diagnostics.json

It only ever creates a new file, 0600, in a folder that already exists. A path that exists (a file or a symlink), a missing folder, or a path inside $COMFY_HOME is refused before the command runs.

The bundle contains fresh request/correlation/trace IDs, bounded operation and dependency outcomes, exit code, and public transaction hashes when a launch is submitted. It never captures the yaml, private key, wallet address, provider body, arbitrary error message, or console output. The CLI sends no telemetry to Comfy or Grafana; it only propagates context headers to the Comfy-owned engine.

| Variable | Default | |---|---| | PRIVATE_KEY | required to launch, to comfy auth login and to comfy compute share, unless --key-file names a file; a dry-run works without it | | WARMTH_API | https://warmth.comfy.fun, comfy.fun's engine (http://localhost:4177 before 0.7.0); the engine sessions, bazaar, task, work, agent and compute status calls go to. A session or work credential is stored per engine, so one made for http://localhost:4177 is used again only with WARMTH_API=http://localhost:4177 | | COMFY_HOME | ~/.config/comfy; holds sessions.json, work.json (where each engine's work credential went), pending/ (register requests), compute-keys.json and compute-spend.json, and, with the file store, credentials/ (dirs 0700, files 0600) | | COMFY_WORK_CREDENTIAL | unset; an agent's work credential. When set, it overrides the one comfy work key store saved | | COMFY_CREDENTIAL_STORE | unset; key store then uses the keychain where there is one, else a 0600 file under $COMFY_HOME/credentials. keychain or file makes it use that store alone | | COMFY_SESSION / COMFY_SESSION_ADDRESS | unset; set both to inject a session in CI. A write needs both; a public read and auth logout work with COMFY_SESSION alone | | COMFY_SITE_ORIGIN / COMFY_CHAIN_ID | unset; pin the site and chain a sign-in challenge may name | | COMFY_ENGINE_ORIGIN | unset; pins the engine allowed to present that site's challenge. Only honoured beside COMFY_SITE_ORIGIN | | COMFY_HTTP_TIMEOUT_MS | 15000; deadline for every engine call, body included | | RPC_URL | required to launch and to comfy compute share (https://evmrpc.0g.ai on 0G mainnet); task award/release/dispute fall back to http://localhost:8545 | | ADDRESSES_PATH | required to launch and to comfy compute share; the absolute path of the Infinity deployment manifest, as described above |

Launch outcomes

  • A chain mismatch, unreadable RPC or reverted simulation is a failed dry-run.
  • A refusal says in plain words what happened, why, and what to change. The launcher's own errors (DelayBelowMinimum, NotAgentOwner, LaunchFeeNotPaid and the rest), a wallet without the 0G for the fee and gas, and a gas price that moved each have their own words; the error's name and selector follow in parentheses. comfy deploy … --debug adds viem's full error, calldata included. It names your RPC_URL, so do not paste it where a key in that URL would leak.
  • A send that fails once the signed launch has reached the RPC says the launch may have been sent and prints its transaction hash: check the wallet's recent transactions before launching again, because a plain launch run twice is a second token and a second fee. It says "not sent" only when nothing reached the RPC, or when the node refused the transaction itself (no 0G for the fee and gas, or a gas limit or fee no node takes). Once the launch has reached the RPC, even that refusal prints the hash and sends you to the wallet first: it is only what the answering node said.
  • A submitted transaction with an unreadable receipt may have succeeded. The CLI prints its hash; verify it before retrying to avoid a second launch.
  • Tickers are not unique onchain. Every invocation uses a fresh salt.
  • A confirmed receipt without the expected launcher/association events is reported for inspection, not treated as permission to launch again.

Keys

The CLI reads PRIVATE_KEY from the environment, or — for auth login, compute share and an Infinity deploy — from the file named by --key-file. It never takes a key as an argument or from agent.yaml. It will not search your filesystem, shell history or other configs for one, it never prints one, and if it has none it stops and says so.

Under $COMFY_HOME it writes the session bearer, a register request's device code until --complete fetches its credential, and, when the store uses the file, an agent's work credential. That bearer can act as your wallet at the engine until it expires, which is why its file is 0600 and why an over-permissive one is a refusal rather than a warning. comfy auth logout revokes it at the engine as well as removing it, and says so — separately — in its exit code and its JSON.

MIT.