DOCUMENTATION
Provision a chain. Connect your app.
This page covers using Signet Sandbox. Setup for people who change the code lives in the repository, which the GitHub link in the header opens.
Install
The CLI is one binary named signet. One line installs it: no repository clone, no sudo.
$ curl -fsSL https://nully0x.github.io/signet-sandbox/install.sh | sh
The script detects your platform, verifies the release checksum, and installs into ~/.local/bin (override with SIGNET_INSTALL_DIR). It covers linux x86_64/arm64 and macOS x86_64/arm64; windows is not supported.
Upgrade
Re-run the same line. The script skips work when the newest release is already installed.
Pin a version
The default line always installs the newest release. To install an exact one, pass --version with any tag from the releases page; v0.1.0 here is an example.
$ curl -fsSL https://nully0x.github.io/signet-sandbox/install.sh | sh -s -- --version v0.1.0
Uninstall
$ curl -fsSL https://nully0x.github.io/signet-sandbox/install.sh | sh -s -- uninstall
Uninstalling removes only the binary at ~/.local/bin/signet. Your identity and connection profile at ~/.signet are kept; delete that directory too if you want a full cleanup. From a repository clone, scripts/install.sh uninstall does the same thing.
Docker is the only runtime prerequisite. Start the daemon before you provision anything.
Getting started
Four steps take you from an installed CLI to a running chain with a funded address.
Step 1. Bootstrap the stack
bootstrap prepares the local platform, starts the API with its database, and writes a profile. That profile identity owns every environment you create. The API also serves the dashboard.
$ signet bootstrap
Pass the advertised host when other machines must reach the endpoints:
$ signet bootstrap --host <host machine ip>
Endpoints default to localhost, which limits them to this machine. The advertised host is the endpoint host, the certificate name, and the explorer hostname base. Choose it at the first bootstrap, because the flag does nothing on a stack that already runs. To change it later, run signet bootstrap --host again with the new value, then mint new credentials for the new URLs.
Step 2. Check the stack
$ signet status
The report covers the local platform, API health, and the identity that will own your environments.
Step 3. Provision an environment
$ signet up --name my-app --ttl 2h --ui
Expect about a minute on the first run, because the signer premines 101 blocks. A warm stack provisions in about 20 seconds. --ui opens the dashboard once the environment is ready; drop it when you work from the terminal only.
up also starts the stack itself when the API is not running, and forwards its own --host to that bootstrap. Run bootstrap first when the host must be set, or when you want the stack running before the first environment.
Step 4. Read the bundle and fund an address
Provisioning prints the connection bundle. Read it again at any time, then send coin to an address:
$ signet get my-app --json $ signet fund my-app tb1qexampleaddress --amount 100000
The bundle carries no address. Generate one from something that tracks this chain:
- Your own wallet, connected to
indexer_endpointor to the node RPC. - The environment's node, which answers
getnewaddressoverrpc_url, authenticated withrpc_auth. - A Lightning instance, with
lncli newaddress p2wkhin the environment, orGET /v1/newaddresswith the bundle macaroon.
$ curl -s --user "$RPC_AUTH" -H 'content-type: text/plain;' \
--data-binary '{"jsonrpc":"1.0","id":1,"method":"getnewaddress","params":[]}' \
"$RPC_URL"
Use an address from a wallet that tracks this chain. A key from another chain gives you coin you cannot see.
Every command that takes an environment accepts the id or the name. Names resolve on the server.
Tear down
$ signet teardown
Teardown deletes the local platform and every environment on it. The profile is kept, so the identity keeps ownership of what you create next.
Configure
Pass flags, or keep a file named sandbox.json in the working directory and up reads it. An explicit --config path also works, and --name and --ttl override the file.
{
"name": "my-app",
"ttl": "2h",
"components": {
"indexer": true,
"explorer": false,
"faucet": true,
"lightning": [
{ "name": "ln-alice", "implementation": "lnd" },
{ "name": "ln-bob", "implementation": "lnd" }
]
},
"versions": { "bitcoind": "29.4" }
}
| Field | Meaning |
|---|---|
| name | Environment name, unique per identity |
| ttl | Human duration: 30m, 2h, 1d. The API field is ttl_secs |
| components | indexer, explorer, and faucet are booleans. lightning is a list of named instances |
| versions | Pin component tags. Keys are component names, values are tags |
The chain and its block signer always come up. Everything else is optional, and a disabled component is omitted from the bundle rather than returned as null.
Only lnd is available as a Lightning implementation. Instance names are DNS labels: lowercase letters, digits, and hyphens, up to 63 characters.
Print the full schema when you need field-level detail:
$ signet schema
The bundle
One object carries every endpoint and every credential your application needs.
| Field | Use |
|---|---|
| rpc_url | Bitcoin Core JSON-RPC |
| rpc_auth | Basic auth value, user:password |
| indexer_endpoint | Electrum endpoint for wallets, when the indexer is enabled |
| explorer_url | Browser view of blocks and transactions |
| faucet_url | Faucet, or use signet fund |
| lightning | Map keyed by instance name, each entry described below |
| signet_challenge | The challenge for this chain. Two environments never share one |
| block_policy | interval_30s or on_demand |
| expires_at | Wall-clock expiry, when a TTL was set |
Connect an application
Bitcoin RPC
Point your client at rpc_url and send rpc_auth as basic auth. The node runs on a custom signet, so the client must not expect the public signet chain.
Electrum wallet
Connect to indexer_endpoint. It is a plain endpoint with no authentication, reachable from the machine that runs the stack.
Explorer
Open explorer_url in a browser. The platform routes by hostname, so an explorer hostname needs a hosts entry when you open it from another machine.
Faucet
Fund any address that belongs to that chain. Use the CLI, or post to faucet_url with the environment credentials.
Remote callers
Endpoints default to localhost. When another machine must connect, set the advertised host at the first bootstrap, as shown in Getting started, and read the bundle again for the new URLs.
Lightning
Each instance is reachable by port, not by name. Three consecutive ports serve REST, gRPC, and peering. The range is 21000 to 21199.
rest https://localhost:21000 grpc https://localhost:21001 p2p localhost:21002
Each entry in the map carries rest_url, grpc_url, tls_cert, the auth object with a macaroon, node_id, and node_uri.
Add a node while it runs
environment.update adds lightning instances to a live, ready environment without tearing it down. The lightning array is the complete desired set: include every instance that already exists, because omitting one is rejected as a removal. v1 is add-only — removals, ttl changes, and other components stay create-time decisions. New instances join the bundle's lightning map once their wallets are ready.
$ curl -s -X POST localhost:8081/api/rpc \
-H 'content-type: application/json' \
-H "Authorization: Bearer $SIGNET_TOKEN" \
-d '{
"jsonrpc": "2.0", "id": 1,
"method": "environment.update",
"params": {
"id": "my-env",
"components": {
"lightning": [
{ "name": "ln-alice", "implementation": "lnd" },
{ "name": "ln-carol", "implementation": "lnd" }
]
}
}
}'
Authentication and TLS
The macaroon ships hex-encoded. REST clients send it as the Grpc-Metadata-macaroon header. gRPC clients send the macaroon and trust the certificate.
The bundle's tls_cert is the CA, hex-encoded PEM. Decode it and pin it as a root. The served leaf chains to it and covers localhost, the internal service names, and the advertised host. Do not disable verification, because the environment exists to behave like the real thing.
Peering and channels
Instances peer automatically after create. Channels stay manual, because channel liquidity is usually the thing under test. Open, pay, and close with lncli run in the environment, or from a client that uses node_uri.
Nodes start with an empty wallet. Fund an address first, then open a channel.
Manage and tear down
| Command | Effect |
|---|---|
| signet ls | List your environments with status and expiry |
| signet get | Print the bundle for one environment |
| signet fund | Send coin from the faucet to an address |
| signet stop | Scale compute to zero and keep storage. The chain survives |
| signet start | Resume a stopped environment |
| signet down | Delete the environment and its chain |
| signet delete | Remove the record of a destroyed or expired environment |
| signet status | Report the local stack, API health, and identity |
| signet teardown | Delete the local stack. The profile is kept |
Expiry
An environment created with a TTL carries an expiry time. A background reaper deletes the environment after that time passes. Stopping an environment does not pause the clock, so a stopped environment still expires.
Names are scoped to your identity. A name held by a destroyed or expired record is free to use again.
Call the API
The CLI is one client. For CI and applications, call the same API directly: JSON-RPC on POST /api/rpc, with a health check on GET /healthz. Failures are error objects inside HTTP 200, never HTTP status codes.
-32002- Unauthenticated. The header is missing, stale, or does not match the URL or method.
-32004- Not the owner. Environments belong to the identity that created them.
-32602- Invalid params. An unknown version key, a bad instance name, or a non-positive
ttl_secs. -32030- Wrong lifecycle state. Stop requires
ready, and start requiresstopped.
Authentication
Mint a bearer token and send it in the Authorization header. The token is shown once, and only its hash is stored.
$ signet token $ export SIGNET_TOKEN=sgn_...
The CLI reads the token from the environment or from --token, and it reads the API base URL from SIGNET_API or --api. Bearer callers cannot mint new tokens, so a new token needs the identity that owns the environments.
The alternative is a signed request. Send a Nostr event of kind 27235 as Authorization: Nostr base64(event), with a u tag holding the exact request URL and a method tag holding the HTTP verb. Events expire after 300 seconds, so mint one per request.
Create an environment
$ curl -s -X POST localhost:8081/api/rpc \
-H 'content-type: application/json' \
-H "Authorization: Bearer $SIGNET_TOKEN" \
-d '{
"jsonrpc": "2.0", "id": 1,
"method": "environment.create",
"params": {
"name": "ci-env",
"components": { "indexer": true, "faucet": true },
"versions": { "bitcoind": "29.4" },
"ttl_secs": 1200
}
}'
The response echoes the resolved versions and the expiry time. The same calls drive the CLI, so a pipeline can create an environment, read the bundle, run its tests, and call environment.destroy.
Agent skill
Coding agents can drive the sandbox for you. The repository ships a skill at skills/signet-sandbox/SKILL.md: the same workflow this page documents, compressed for agents — bundle shape, endpoints, funding, lifecycle, multi-node channels, and the LND REST quirks. Reference the file from your agent, or copy it into your agent's skills directory.
Troubleshooting
The CLI cannot reach the API
Start the local stack with signet up, or check what is running with signet status.
The first environment is slow
The signer premines 101 blocks before the chain is usable. Expect about a minute on the first run, and about 20 seconds with a warm stack.
An endpoint works on one machine only
Endpoints default to localhost. Re-bootstrap with --host set to an address the other machine can reach, then read the bundle again for the new URLs.
A signed request fails after a host change
The signed URL must match the URL the server receives. Mint a new header for the new URL.
The explorer does not open from another machine
The platform routes by hostname, so add a hosts entry for the environment's explorer hostname on the machine that opens it.
A Lightning name is rejected
Use lowercase letters, digits, and hyphens, start with a letter or digit, and keep the name within 63 characters. Names must be unique inside one environment.
A channel will not open
Fund the node before opening a channel. Instances peer automatically, but they start with an empty wallet and no channels.
An environment disappeared
The TTL expired and the reaper removed it. Stopping does not pause the clock, so choose a TTL that covers the work.