Sandbox
The mock reserve source behind the demo. OAuth, signed balances in USD, the accounts, the scenarios and how to seed them.
The sandbox is a mock reserve source. It stands in for a custodian that signs the balances it holds, so you can run full cycles without connecting a real one. It lives in services/sandbox.
The orchestrator reads the sandbox through the same adapter it uses for any attested reserve source, infrastructure/attested, and verifies every signature. Only the balance figures are synthetic. Connections to real custodians Coming soon use the same signed-balance model.
Run it
cd services/sandbox && go run ./cmd/app| Variable | Default | Meaning |
|---|---|---|
SANDBOX_HTTP_ADDR | :8090 | Listen address. |
SANDBOX_SIGNING_KEY_PEM | empty | P-256 private key in PEM. When empty, the sandbox creates a new key at boot and logs its public key. |
SANDBOX_LOG_LEVEL | info | Log level. |
The orchestrator must trust the sandbox's key. Set ORCH_RESERVE_PUBLIC_KEY_PEM on the orchestrator to the sandbox's public key. just e2e and just demo do this for you by fetching it from GET /.well-known/solva-signing-key.pem. With an ephemeral key, restart both services together, or pin the key in both.
Routes
| Method and path | Auth | What it does |
|---|---|---|
GET /oauth/authorize?client_id=<id> | none | Returns {"code": "..."}, a one-time code valid for 5 minutes. Consent is granted at once. |
POST /oauth/token | none | Form body code=<code>. Returns {"access_token", "token_type": "Bearer", "expires_in": 3600}. The code is used up. |
GET /v1/accounts/{id}/balance | Authorization: Bearer <access_token> | Returns a signed balance. |
POST /admin/scenarios/{name} | none | Sets every account to a named scenario. |
GET /.well-known/solva-signing-key.pem | none | The public key that verifies the signatures, PEM. |
GET /health | none | {"status":"ok"}. |
The orchestrator authenticates with the client id in ORCH_RESERVE_CLIENT_ID, default solva-orchestrator, once per cycle, then reads every account in parallel.
Signed balances
curl http://localhost:8090/v1/accounts/acct-anchor/balance \
-H "Authorization: Bearer $TOKEN"{
"source_id": "acct-anchor",
"balance": "8000000",
"currency": "USD",
"as_of": "2026-11-04T09:00:00Z",
"signature": "MEUCIQ..."
}balance is a decimal string of integer minor units in USD: cents, so "8000000" is 80,000.00 US dollars. as_of is the time of signing in RFC 3339, UTC.
The signature scheme is fixed:
- Curve P-256 (secp256r1).
- SHA-256 over the canonical payload.
- An ASN.1 DER signature, base64 encoded.
The canonical payload is the JSON object of the four signed fields with keys in sorted order and no whitespace, and without signature:
{
"as_of": "2026-11-04T09:00:00Z",
"balance": "8000000",
"currency": "USD",
"source_id": "acct-anchor"
}The orchestrator rebuilds these bytes from the response and verifies the signature with its configured public key. A bad signature fails the cycle and is never retried. Network errors and 5xx responses are retried, with 3 attempts in all and a backoff that starts at 200 ms and doubles.
Accounts
Three mock custodians hold one account each:
| Account | Custodian |
|---|---|
acct-anchor | custodian-anchor |
acct-beacon | custodian-beacon |
acct-cedar | custodian-cedar |
The orchestrator reads the accounts listed in ORCH_RESERVE_ACCOUNTS, default acct-anchor,acct-beacon,acct-cedar. An unknown account returns 404.
Scenarios
A scenario sets the balance of every account at once. The scenarios are chosen against the demo's liabilities, L = 9,000,000, which just e2e and just demo seed as three customers owed 4,000,000, 3,000,000 and 2,000,000.
| Scenario | acct-anchor | acct-beacon | acct-cedar | Reserves R | R against L | Outcome |
|---|---|---|---|---|---|---|
solvent | 8,000,000 | 5,000,000 | 3,000,000 | 16,000,000 | 78% above | Covered. Proof published. |
near-breach | 4,200,000 | 3,100,000 | 2,200,000 | 9,500,000 | 5.6% above | Covered with a thin margin. Proof published. |
insolvent | 2,000,000 | 1,500,000 | 1,000,000 | 4,500,000 | below | Not covered. The orchestrator returns 422 and nothing is published. |
A fresh sandbox starts on solvent. Scenarios are deterministic: the same name always gives the same balances.
Seed one:
curl -X POST http://localhost:8090/admin/scenarios/near-breach{ "scenario": "near-breach", "status": "seeded" }An unknown name returns 400 with the list:
{ "error": "unknown scenario", "available": ["solvent", "near-breach", "insolvent"] }Then run a cycle with POST /v1/cycles.
Switching scenarios
The circuit's growth bound lets reserves rise at most 10% from one proof to the next (see Proof format). Reserves may fall by any amount, but a rise of more than 10% fails:
| From | To | Result |
|---|---|---|
solvent (16.0M) | near-breach (9.5M) | Published. Reserves fell. |
| any | insolvent (4.5M) | Refused with 422. Nothing is published, so the last proof does not change. |
near-breach (9.5M) | solvent (16.0M) | Fails with 500. 16,000,000 is more than 1.1 × 9,500,000 = 10,450,000. |
After a near-breach proof, solvent cannot publish on that registry again. To get back to a clean state, start a fresh run. just e2e and just demo reset the database and deploy new registries unless you supply existing ones.
The admin route has no authentication. Anyone who can reach the sandbox can change its balances. Run it only where that is acceptable.
Next: AI tools.