> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stellarium.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Run a local network

> Run the operator, escrow program, and a caller and provider node on a local Solana validator to test paid requests end to end.

This guide runs a complete paid Stellarium setup on your machine from the [stellarium-ts](https://github.com/qualletio/stellarium-ts) monorepo:

| Service | Port | Role |
| - | - | - |
| Solana test validator | `8899` | Local chain with the escrow program |
| Operator | `3001` | Creates escrows and settles payments |
| `local-resource-node` | `3002` | Hosts a paid `com.example.Weather` resource |
| `local-caller-node` | `3003` | Has no resources; bootstraps from the resource node and pays for calls |
| PostgreSQL | — | Operator receipts |

All money is local test SOL. Never use funded wallets.

## Prerequisites

* Node.js 20+ and pnpm
* Rust and the [Solana CLI](https://solana.com/docs/intro/installation), including `cargo build-sbf` and `solana-test-validator`
* Docker with the daemon running

```bash theme={null}
git clone https://github.com/qualletio/stellarium-ts
cd stellarium-ts
pnpm install
```

<Steps>
  <Step title="Start the validator">
    In its own terminal:

    ```bash theme={null}
    solana-test-validator
    ```

    Then point the Solana CLI at localnet:

    ```bash theme={null}
    solana config set -ul
    ```
  </Step>

  <Step title="Generate local keys">
    Create Solana keypairs for the operator, the escrow program, the provider's recipient wallet, the protocol fee wallet, and a caller wallet:

    ```bash theme={null}
    umask 077
    mkdir -p .keys/local-e2e
    for name in operator program recipient protocol-fee caller; do
      solana-keygen new --no-bip39-passphrase --silent --outfile .keys/local-e2e/$name.json
    done
    ```

    Then generate node signing keys and `.env` values for the caller, provider, and operator. This writes `caller.env`, `provider.env`, and `operator.env` into `.keys/local-e2e/` without printing secrets:

    ```bash theme={null}
    node --input-type=module <<'JS'
    import { generateKeyPairSync, randomBytes } from 'node:crypto';
    import { readFileSync, writeFileSync } from 'node:fs';
    import { createRequire } from 'node:module';
    const require = createRequire(new URL('./apps/operator/package.json', `file://${process.cwd()}/`));
    const { PublicKey, Keypair } = require('@solana/web3.js');
    const address = name => Keypair.fromSecretKey(Uint8Array.from(JSON.parse(
      readFileSync(`.keys/local-e2e/${name}.json`, 'utf8')))).publicKey.toBase58();
    const common = {
      SOLANA_RPC_URL: 'http://127.0.0.1:8899',
      ESCROW_PROGRAM_ID: address('program'),
      AUTHORITY_ADDRESS: address('operator'),
      RECIPIENT_ADDRESS: address('recipient'),
      PROTOCOL_FEE_RECIPIENT: address('protocol-fee'),
    };
    for (const [role, port] of [['caller', 3000], ['provider', 3002]]) {
      const { publicKey, privateKey } = generateKeyPairSync('ed25519');
      const spki = publicKey.export({ format: 'der', type: 'spki' });
      const raw = Buffer.from(publicKey.export({ format: 'jwk' }).x, 'base64url');
      const values = { ...common, BASE_URL: `http://127.0.0.1:${port}`,
        OPERATOR_URL: 'http://127.0.0.1:3001/v1',
        SIGNING_PUBLIC_KEY: spki.toString('base64url'),
        SIGNING_PRIVATE_KEY: privateKey.export({format: 'der', type: 'pkcs8'}).toString('base64url'),
        USER_ADDRESS: new PublicKey(raw).toBase58() };
      writeFileSync(`.keys/local-e2e/${role}.env`, Object.entries(values)
        .map(([key, value]) => `${key}=${value}`).join('\n') + '\n', { mode: 0o600, flag: 'wx' });
    }
    const values = { ...common,
      DATABASE_URL: 'postgresql://stellarium:local-only-password@127.0.0.1:5433/stellarium',
      OPERATOR_API_TOKEN: randomBytes(32).toString('hex'),
      PRIVATE_KEY: readFileSync('.keys/local-e2e/operator.json', 'utf8').trim() };
    writeFileSync('.keys/local-e2e/operator.env', Object.entries(values)
      .map(([key, value]) => `${key}=${value}`).join('\n') + '\n', { mode: 0o600, flag: 'wx' });
    console.log('Local fixture configuration written; no secrets printed.');
    JS
    ```
  </Step>

  <Step title="Configure and build the escrow program">
    The escrow program has the operator's address and the protocol fee wallet compiled in. Print both:

    ```bash theme={null}
    solana-keygen pubkey .keys/local-e2e/operator.json
    solana-keygen pubkey .keys/local-e2e/protocol-fee.json
    ```

    Set them in `programs/escrow_program/src/lib.rs`:

    ```rust theme={null}
    pub const ESCROW_AUTHORITY: Option<Pubkey> =
        Some(solana_program::pubkey!("YOUR_OPERATOR_ADDRESS"));
    pub const PROTOCOL_FEE_RECIPIENT: Option<Pubkey> =
        Some(solana_program::pubkey!("YOUR_PROTOCOL_FEE_ADDRESS"));
    ```

    Build it:

    ```bash theme={null}
    cargo build-sbf --manifest-path programs/escrow_program/Cargo.toml \
      --sbf-out-dir programs/escrow_program/target/deploy
    ```
  </Step>

  <Step title="Fund wallets and deploy">
    The operator pays for the deploy and escrow setup. The recipient and fee wallets are prefunded so small payments don't fail the rent check:

    ```bash theme={null}
    solana airdrop 100 "$(solana-keygen pubkey .keys/local-e2e/operator.json)"
    solana airdrop 5 "$(solana-keygen pubkey .keys/local-e2e/caller.json)"
    solana airdrop 1 "$(solana-keygen pubkey .keys/local-e2e/recipient.json)"
    solana airdrop 1 "$(solana-keygen pubkey .keys/local-e2e/protocol-fee.json)"

    solana program deploy \
      --keypair .keys/local-e2e/operator.json \
      --upgrade-authority .keys/local-e2e/operator.json \
      --program-id .keys/local-e2e/program.json \
      programs/escrow_program/target/deploy/escrow_program.so
    ```
  </Step>

  <Step title="Create .env files">
    Copy values from the generated files in `.keys/local-e2e/` into each app.

    <CodeGroup>
      ```bash apps/local-caller-node/.env theme={null}
      SIGNING_PUBLIC_KEY=
      SIGNING_PRIVATE_KEY=
      USER_ADDRESS=
      PORT=
      API_KEY=SET_THIS_TO_ANYTHING
      ```

      ```bash apps/local-resource-node/.env theme={null}
      SIGNING_PUBLIC_KEY=
      SIGNING_PRIVATE_KEY=
      USER_ADDRESS=
      RECIPIENT_ADDRESS=
      PORT=
      API_KEY=SET_THIS_TO_ANYTHING
      ```

      ```bash apps/operator/.env theme={null}
      ESCROW_PROGRAM_ID=
      PROTOCOL_FEE_RECIPIENT=
      PRIVATE_KEY=
      PORT=
      ```
    </CodeGroup>

    `PROTOCOL_FEE_RECIPIENT` must match the address compiled into the program.
  </Step>

  <Step title="Start the services">
    ```bash theme={null}
    docker compose up -d --build
    ```
  </Step>

  <Step title="Create and fund the caller's escrow">
    Ask the operator to create an escrow for the caller's `USER_ADDRESS`:

    ```bash theme={null}
    curl -X POST http://localhost:3001/v1/escrow \
      -H 'Content-Type: application/json' \
      -d '{"user":"YOUR_USER_ADDRESS"}'
    ```

    ```json Response theme={null}
    {
      "escrowAddress": "ESCROW_ADDRESS",
      "signature": "SIGNATURE",
      "status": "confirmed"
    }
    ```

    Fund it from the caller wallet:

    ```bash theme={null}
    solana transfer ESCROW_ADDRESS 0.1 \
      --from .keys/local-e2e/caller.json \
      --fee-payer .keys/local-e2e/caller.json \
      --allow-unfunded-recipient
    ```
  </Step>

  <Step title="Run a paid request">
    Call the caller node, which routes to the paid resource on the resource node:

    ```bash theme={null}
    curl -X POST http://localhost:3003/v1/run \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer YOUR_CALLER_API_KEY' \
      -d '{"source":"request MyRequest {\n const weather: com.example.Weather\n return weather.temperature(city: \"Oslo\")\n}"}'
    ```

    ```json Response theme={null}
    { "returnValue": 12 }
    ```

    Settlement runs after the result is returned, so check the operator logs and the recipient's balance to confirm payment. A `returnValue` alone doesn't prove the payment settled.
  </Step>
</Steps>

## Troubleshooting

If a request fails, check each `.env` file, then rerun `docker compose up -d --build`.

| Symptom | Cause |
| - | - |
| Custom program error `0` | Program built with `ESCROW_AUTHORITY` unset. Configure, rebuild, redeploy. |
| Custom program error `1` | Compiled authority differs from the operator's `PRIVATE_KEY` address. |
| Custom program error `2` | Program built with `PROTOCOL_FEE_RECIPIENT` unset. |
| Custom program error `3` | Operator `PROTOCOL_FEE_RECIPIENT` differs from the compiled fee wallet. |
| `ECONNREFUSED` on 8899 | The validator isn't running. |

For a deeper, source-audited walkthrough including on-chain verification, see [`docs/local-e2e.md`](https://github.com/qualletio/stellarium-ts/blob/main/docs/local-e2e.md) in the repository.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.