@singularity-layer/cli
v0.3.0
Published
Singularity developer CLI — deploy paid endpoints (Processors) on the Singularity Cloud Network
Maintainers
Readme
singularity — Singularity developer CLI
Build on the Singularity Cloud Network from your terminal.
npm i -g @singularity-layer/cli
singularity --helpRequires Node 20+ and either a Solana keypair (solana-keygen new) or a compute API key. No
account and no dashboard login — your wallet is the account.
Processors
A Processor is a loop you write; we host it, run it on demand, and buyers pay you directly per call. There is no platform cut and we never custody your money: an x402 payment is a USDC transfer from the buyer's wallet to yours.
mkdir my-loop && cd my-loop
singularity processors init my-loop # scaffolds processor.json + processor.js
singularity processors deploy # prints the invoke token ONCE
singularity processors run '{"hi":1}' # invoke it, print the output
singularity processors publish # instantly public — no review step
singularity processors logs # runs + failure rate
singularity processors revenue # sales and compute spend, everything you ownYour loop is a standard Worker module:
export default {
async fetch(request) {
const { input } = await request.json();
// Call APIs and models with fetch(). Credentials for the hosts you declared
// are injected for you, so no keys belong in this file.
return Response.json({ ok: true, received: input });
}
}TypeScript and npm packages
Add --bundle (or "$bundle": true in processor.json) and the CLI runs esbuild locally, so
import and npm dependencies work:
npm i zod
singularity processors deploy --bundle --entry processor.tsThe bundling happens on your machine, on purpose. We never run npm install for you: a
postinstall script is arbitrary code, and running it on infrastructure that holds platform
credentials is not something scanning makes safe. Your bundle crosses the boundary; your
node_modules never does.
The bundler targets a Worker isolate, so a dependency reaching for fs, child_process or raw
sockets fails here, on your terminal, naming the importer — instead of dying inside a run a buyer
has already paid for.
Going back to a single file needs --no-bundle, because a stored bundle keeps winning at load time
and plain code would silently never take effect.
Invoking
singularity processors run '{"n":10}' # JSON in, output out
singularity processors call -i # prompts per field, from your own input_schema
singularity processors schema # what buyers and agents seeSecrets
Declare a secret in processor.json, then set its value:
singularity processors secrets OPENAI_API_KEY=sk-...
singularity processors env list # which are declared, which are set
singularity processors env unset OPENAI_API_KEYValues are write-only to you: we never hand one back through the API.
Two modes, chosen per secret. The default is the strong one:
"secrets": [
{ "name": "OPENAI_API_KEY", "hosts": ["api.openai.com"],
"inject": { "header": "Authorization", "format": "Bearer {value}" } },
{ "name": "SIGNING_KEY", "mode": "env" }
]inject (the default) — the value never enters your isolate. You call fetch() with no
credential and the egress gateway adds the header server-side, for the hosts you declared and nowhere
else. Code that never holds a key cannot leak the key.
mode: "env" — your code holds the value, via await SGL.secrets.get('SIGNING_KEY'). This is a
real downgrade and it is opt-in for exactly that reason: once your code has the value it can print it
into your logs, or send it to any host in your egress allowlist, and we cannot stop either. Use it
only for credentials that genuinely cannot be an outbound header — a key you sign with locally, or a
value an SDK insists on reading itself. The alternative most people reach for otherwise is
hard-coding the secret in their source, which is worse in every way.
singularity processors env list shows which of yours is which.
Processor state
A processor is a fresh isolate every run, so anything you want to keep has to go somewhere:
// Key/value — a cursor, a cache, a dedupe set. Values are opaque strings, byte-exact.
await SGL.kv.put('cursor', '2026-08-12');
const cursor = await SGL.kv.get('cursor');
// Compare-and-swap, for a counter or a lock. Concurrent runs are the normal case.
const cur = await SGL.kv.getWithVersion('count');
await SGL.kv.put('count', String(Number(cur.value) + 1), { ifVersion: cur.version });
// Objects — a generated PDF, an image, a dataset.
await SGL.files.put('reports/august.pdf', bytes, { contentType: 'application/pdf' });
const { url } = await SGL.files.downloadUrl('reports/august.pdf', { ttlSeconds: 3600 });downloadUrl gives you a signed, expiring link a buyer can fetch with no credential. It is
always served as an attachment with a forced application/octet-stream type — your declared type is
never echoed — so a stored HTML or SVG file cannot execute. Treat it as a bearer link: anyone who
gets it can download until it expires.
None of this needs an egress.allow entry. It is not the network.
Turning it off without losing it
singularity processors pause # buyers and token callers refused; no payment is taken
singularity processors resumepause is the switch to reach for when an upstream key hits its quota or a loop misbehaves.
Unlisting only hides you from the catalogue — the endpoint keeps answering anyone with the URL — and
delete burns the slug forever. While paused you can still invoke it yourself with your wallet, so
you can verify a fix before resuming.
For scripts and agents
Every command takes --json:
singularity processors list --json | jq '.processors[] | select(.paused_at)'
singularity processors run '{"n":1}' --jsonAuth
Two ways in. The CLI picks the API key when one is given, and the keypair otherwise.
Wallet signature (default). Uses ~/.config/solana/id.json (--keypair <path>,
SINGULARITY_KEYPAIR, or singularity processors configure to change it). The key never leaves
your machine — only a signature is sent, and it covers the method, the path and a hash of the
body, so it authorizes exactly one request and cannot be replayed against another.
Compute API key. --api-key x402c_..., or SGL_API_KEY in the environment. This is the option
for CI and for agents, where there is no keypair to hold.
export SGL_API_KEY=x402c_...
singularity whoami # the wallet the key resolves to
singularity processors list
singularity processors deployMint the key in the dashboard with the processors:write scope, or processors:read if you only
need the read commands. Two things to know before you rely on it:
processors:writeis full control of that wallet's processors, delete and secrets included. A bearer credential cannot be bound to one request the way a signature is, so scope is the only control. Mintprocessors:readif you want something that cannot change anything.deployneeds a key minted on a Solana wallet. That address becomes your payout account, so a key on an EVM wallet gets400 solana_wallet_required. It can still manage processors its wallet already owns.
An API key never expires, and there is no audit log of what it did. Revoking it in the dashboard is the only way to stop it.
Programmatic use
import { bundleProcessor, api, loadKeypair, usd } from '@singularity-layer/cli';The exported pieces are the ones that are easy to get subtly wrong: the signature scheme, the
micro-USDC formatting (never use floats for money — the ledger is integers), and the bundler
configuration. Subcommands are deliberately not exported; they call process.exit.
The other Singularity CLIs
These are separate on purpose — different audiences, different runtimes.
| CLI | Who it's for |
|---|---|
| singularity | Developers building on the platform |
| sglcode | Agentic coding on the grid |
| sgl | Node operators running compute and earning |
New capabilities here become subcommands, never new binaries.
