Using Juice from a program
A program can use Juice through the command-line client or directly through the HTTP API. Both use the same account permissions and payment rules as an interactive user. Automation requires particular care with login selection, units, errors, and retries, because the program must make decisions that a person would otherwise make at the terminal.
Give an agent its own account so that its balance and history can be managed independently. The account grants no special privileges; it authorizes the agent’s calls in the same way as any other user’s.
Name the login on every command
Specify the saved login for each invocation with --as:
$ juice --as bot@acme run sys/lookup '{"query":"translate to german"}' --json
Alternatively, set JUICE_AS=bot@acme in the environment.
Explicit selection keeps later invocations tied to the intended account even when a person changes the client’s current login. If the named login does not exist, the command fails instead of selecting another account.
For persistent records, identify the kernel by public key and network digest
and the account by its ID. You can obtain these from kernel list and
user me. Handles and local kernel names are useful for interaction but may
be renamed or reused.
Output
Use --json when the program needs to parse a successful response. It preserves
the server’s reply structure. Use --quiet when only the returned identifiers
are needed, one per line:
$ juice --as bot@acme run sys/time --json
{
"result": { "iso": "2026-09-14T12:06:52Z", "unix": 1789387612 },
"tx_id": "98bb64e6-…",
"trace_id": "f4fbbf15-…",
"receipt_id": "0d60120d-…",
"process_id": "e9dbb283-…"
}
$ juice --as bot@acme run sys/time --quiet
821a9f33-…
Handling errors
The command line reports an error on stderr and sets an exit status. The
--json option applies to successful replies, so it does not make command-line
errors machine-readable JSON. Branch on the exit status using these meanings:
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | any other failure |
| 2 | not authenticated |
| 3 | not authorised |
| 4 | not found |
| 5 | invalid input, or schema violation |
| 6 | insufficient funds |
| 7 | timeout |
| 8 | consent required |
| 9 | peer provably unreachable |
| 10 | peer will not serve on credit |
| 11 | terms changed |
Message text is intended for people and should not be used as a program’s error
classifier. Over HTTP, the response instead contains a stable code, a message,
and, where applicable, meta:
{"code":"schema_violation","error":"field #.msg: required field missing"}
{"code":"grant_required","error":"grant required for bob/mail","meta":{"action":"bob/mail"}}
The metadata supplies context for recovery. For example, grant_required
identifies the action requiring consent, while peer errors identify the remote
kernel. A pending call includes process_id, pending_since, and
refund_eligible_at, allowing the program to follow its existing execution.
Units
Convert amounts at the interface boundary. The command line accepts display units, while the HTTP API and action arguments and results use integer base units. On the shipped networks, one display unit contains 1,000,000 base units:
$ juice user transfer bob 1.5 # display units
POST /v1/run {"action":"sys/transfer","args":{"target":"bob","amount":1500000}}
Both examples deliver the same amount, although sys/transfer may also have
an execution price. Similarly, GET /v1/me returns "available": 4795000
where the command line displays 4.795 credits. Read decimals from
GET /health when calculating conversions instead of hard-coding six.
Separate planning from spending
The built-in sys/llm/decide action can choose among candidate actions and
propose their arguments. Calling it purchases the selection service, but does
not execute the action it selects:
$ juice --as bot@acme run sys/llm/decide '{
"messages": [{"role":"user","content":"summarise this contract"}],
"actions": ["bob/echo","dave@beta-kernel/summarize"]
}' --json
The result contains {"action": …, "args": …}. The kernel resolves each
candidate’s actual contract and validates proposed arguments against its schema.
An unreachable remote candidate can be discarded so that selection continues
among available choices; an unavailable model produces an invalid-state error.
Your program can inspect the proposal before issuing a separate run. This
keeps the decision to spend on the selected service under the program’s control.
Pin the terms between reading and running
An action’s terms can change while a program prepares work. Carry the
quote_hash from the selected search result or action read into the execution
request:
$ juice --as bot@acme run bob/echo '{"msg":"hi"}' --quote-hash 4965342976414282…
A mismatch on an otherwise callable action produces exit code 11 before any charge. The program can then obtain the new terms and decide whether to proceed. If the action is inactive, that earlier precondition fails instead.
Retries
Repeating run starts another purchase. The public run request has no
idempotency key, so a program must establish the outcome of an earlier request
before deciding whether to submit it again. Remote transport retries within
the kernel are different: they retain the original call’s identity.
- A cross-kernel call that parked returns
process_id,pending_sinceandrefund_eligible_at. Polljuice process show <id>until it closes. A running kernel settles it when the receipt arrives, or as a refunded failure oncerefund_eligible_athas passed; a kernel that is stopped settles nothing until it is started again. - A call that failed with exit code 9 provably never left your kernel and was fully refunded. It is safe to retry.
Transfers and withdrawals provide explicit retry keys: --external-key for a
transfer and --id for a withdrawal. Generate and save the key before issuing
the request, then reuse it with the same terms if a retry is needed. The kernel
returns the existing movement rather than creating a second one.
$ juice --as bot@acme user transfer bob 1 --external-key payout-2026-09-14-001 --yes
$ juice --as bot@acme user withdraw 5 --id wd-2026-09-14-001 --yes
Step completion also prevents duplicate execution. A repeated local completion is refused after the step has been claimed or completed; read its record to find the resulting transaction. A remote completion derives its retry key from the step and input, allowing the same request to recover its stored outcome.
Confirmation
Transfers and withdrawals require confirmation. Without a terminal or explicit confirmation, the client refuses to act:
$ juice user transfer bob 1
error: re-run with --yes to confirm (no terminal to ask on)
Supply --yes when the program has authorized that movement. An action run
does not prompt: issuing the request authorizes the selected price and any
value named in its arguments.
Non-interactive equivalents
| Interactive | Non-interactive |
|---|---|
password prompt on auth login, user create |
--password |
| first-boot questions | a config.json written in advance, plus JUICE_BOOTSTRAP_PASSWORD |
| confirmation on a money command | --yes |
| the selected login | --as or JUICE_AS |
A password supplied as a command-line argument may be visible to other processes. For long-running programs, establish a saved login during setup and use its managed session for subsequent commands.
Speaking HTTP directly
A direct HTTP client uses the same API as the command line. Before sending
credentials, check GET /health against the expected kernel key and network,
as shown below. Authentication uses an authorization code with PKCE: the
client generates a verifier, sends its derived challenge when authenticating,
and presents the verifier when exchanging the code for tokens.
$ VERIFIER=$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=')
$ CH=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=')
$ curl -s -X POST localhost:4040/v1/auth/authorize -H 'Content-Type: application/json' \
-d "{\"handle\":\"bot\",\"password\":\"…\",\"code_challenge\":\"$CH\"}"
{"redirect":"?code=RF175R2g…"}
$ curl -s -X POST localhost:4040/v1/auth/token -H 'Content-Type: application/json' \
-d "{\"code\":\"RF175R2g…\",\"code_verifier\":\"$VERIFIER\"}"
{"access_token":"eyJhbGciOi…","refresh_token":"…"}
Then use the access token as a bearer:
$ curl -s localhost:4040/v1/run -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"action":"sys/time","args":{}}'
{"result":{"iso":"2026-09-14T12:07:31Z","unix":1789387651},"tx_id":"800c8279-…", …}
Access tokens expire. On a 401, the client can exchange its refresh token at
POST /v1/auth/refresh for a new pair. Persist the replacement refresh token
before the next refresh, since the old one is no longer valid.
Check GET /health before trusting a server, and compare what it reports against
the key and network you expect:
$ curl -s localhost:4040/health
{"decimals":6,"handle":"acme","network":"play","network_digest":"ef1fac03…",
"public_key":"fdlMi64P…","rail_address":"","status":"ok","symbol":"credits"}
This check distinguishes the expected kernel from any other server occupying the same address. Save the expected identity when establishing trust and compare subsequent responses against it.
The full HTTP interface is documented in
API.md.
Kernel-to-kernel federation uses a separate transport managed by the kernel.
Implementing a service
A program implementing an HTTP action has a different role from an agent that buys services. The endpoint receives an execution capability with each dispatched call and can use it to request work within that call’s budget. It therefore needs no saved Juice login for composition. See Composing from an HTTP endpoint.
The action’s owner holds the account that receives its earnings. An agent making independent purchases needs its own account and login to authorize them.