Aleo for Agents is offline at present

skill·2026-08-03·10 min read

Aleo Backend Integration

Aleo backend integration

1. Overview

Backend services on Aleo query on-chain state, orchestrate transactions, scan for records, delegate proof generation, and manage keys. This skill covers @provablehq/sdk in Node.js.

Version and canonical syntax

Target: @provablehq/sdk 0.11.6, Leo compiler >= 4.4.0.

On-chain examples follow Leo 4.4: fn for entry points, final { } blocks for state changes, Final as the return type, .run() to execute a returned finalization handle, :: for cross-program references, and std::ctx::caller() for the caller address.

Numeric inputs to the SDK need Leo type suffixes: "100u64", not "100".

When these examples and the published API reference disagree, check the package's own .d.ts files. The SDK's option shapes have changed within the 0.11.x line.

2. Key concepts

  • AleoNetworkClient: queries the network for mappings, programs, transactions, and blocks.
  • ProgramManager: builds and submits executions and deployments, and manages keys.
  • AleoKeyProvider: stores and caches proving and verifying keys.
  • NetworkRecordProvider: finds unspent records on-chain for an address.
  • RecordScanner: a confidential record-scanning service keyed by UUID and JWT. Scales better than scanning blocks yourself.
  • PreparedProgram: a reusable snarkVM context for one program and function, created with prepareProgram().
  • Authorization: a signed commitment to execute a function, which a remote prover can turn into a transaction.
  • Delegated proving: sending proof generation to a remote service instead of burning local CPU.

3. Setup

bash
npm install @provablehq/sdk@0.11.6
typescript
import {
    Account,
    AleoNetworkClient,
    ProgramManager,
    AleoKeyProvider,
    NetworkRecordProvider,
} from "@provablehq/sdk";

const ENDPOINT = "https://api.explorer.provable.com/v1";

const account = Account.from_string("APrivateKey1...");
console.log("Address:", account.address().to_string());
console.log("View key:", account.viewKey().to_string());

const networkClient = new AleoNetworkClient(ENDPOINT);

const keyProvider = new AleoKeyProvider();
keyProvider.useCache(true);

const recordProvider = new NetworkRecordProvider(account, networkClient);

const programManager = new ProgramManager(ENDPOINT, keyProvider, recordProvider);
programManager.setAccount(account);

The public endpoint is /v1. Import from @provablehq/sdk for the browser-compatible surface, or from the Node entry point when you need LocalFileKeyStore.

4. Reading on-chain state

typescript
const balance = await networkClient.getProgramMappingValue(
    "token.aleo",
    "account",
    "aleo1qnr4dkkvkgfqph0vzc3y6z2eu975wnpz2925ntjccd5cfqxtyu8s7pyjh9",
);
console.log("Balance:", balance);   // "1000u64"

const source = await networkClient.getProgram("token.aleo");
const oldSource = await networkClient.getProgram("token.aleo", 0);  // by edition
const imports = await networkClient.getProgramImports("token.aleo");

const height = await networkClient.getLatestHeight();
const tx = await networkClient.getTransaction("at1...");

Reading public state costs nothing and needs no transaction. Programs that expose a "getter" entry point are a Solidity habit that does not translate: query the mapping directly.

The REST API is there when you do not want the SDK in the loop:

typescript
const BASE = "https://api.explorer.provable.com/v1";
const NETWORK = "testnet";

const value = await (
    await fetch(`${BASE}/${NETWORK}/program/token.aleo/mapping/account/aleo1...`)
).text();

const height = await (await fetch(`${BASE}/${NETWORK}/latest/height`)).text();
const program = await (await fetch(`${BASE}/${NETWORK}/program/token.aleo`)).text();

5. Executing transactions

typescript
const tx = await programManager.buildExecutionTransaction({
    programName: "token.aleo",
    functionName: "transfer_public",
    inputs: ["aleo1receiver...", "100u64"],
    priorityFee: 0,        // microcredits, on top of the base fee
    privateFee: false,      // pay from the public balance
});

const txId = await networkClient.submitTransaction(tx);
console.log("Transaction:", txId);

The base fee is estimated automatically. The old fee option is gone and baseFee is deprecated and ignored, so a call carrying fee: 1_000_000 is silently not doing what it looks like. priorityFee is the only fee knob you set directly.

To know the cost before committing:

typescript
const estimate = await programManager.estimateExecutionFee({
    programName: "token.aleo",
    functionName: "transfer_public",
});
console.log("Estimated fee (microcredits):", estimate);

6. Waiting for confirmation

typescript
async function waitForConfirmation(
    client: AleoNetworkClient,
    txId: string,
    maxAttempts = 60,
    intervalMs = 5000,
) {
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
        try {
            const result = await client.getTransaction(txId);
            if (result) return result;
        } catch {
            // not indexed yet
        }
        await new Promise((resolve) => setTimeout(resolve, intervalMs));
    }
    throw new Error(`Transaction ${txId} not confirmed after ${maxAttempts} attempts`);
}

A transaction can be accepted and still have its final block rejected on-chain. Confirming that the transaction exists is not the same as confirming the state change landed, so read the mapping back when the state change is what you care about.

7. Records

typescript
const records = await recordProvider.findCreditsRecords(
    [5_000_000],              // microcredit amounts sought
    { unspent: true, nonces: [] },
);

for (const record of records) {
    console.log(record.toString());
}

Decrypting with a view key:

typescript
import { ViewKey } from "@provablehq/sdk";

const viewKey = ViewKey.from_string(process.env.ALEO_VIEW_KEY!);
const plaintext = viewKey.decrypt(ciphertextString);

An Account can do it directly, which is convenient when you already hold one:

typescript
const record = account.decryptRecord(ciphertextString);
const allRecords = account.decryptRecords(ciphertextStrings);

NetworkRecordProvider walks blocks looking for records, which is fine for a single account and painful at scale. RecordScanner exists for the scaled case: register a view key with register() or registerEncrypted(), then pull with encryptedRecords() and check spends with checkSerialNumbers().

8. Deploying from a backend

typescript
import * as fs from "fs";

const programSource = fs.readFileSync("./build/main.aleo", "utf8");

const tx = await programManager.buildDeploymentTransaction(
    programSource,
    0,       // priorityFee in microcredits
    false,   // privateFee
);

const txId = await networkClient.submitTransaction(tx);
await waitForConfirmation(networkClient, txId);

buildDeploymentTransaction takes positional arguments, unlike buildExecutionTransaction which takes an options object. This inconsistency catches people; passing an object to the deployment builder fails in a way that does not obviously point at the argument shape.

9. Prepared program contexts

Building an authorization resolves the program source, its edition, and its transitive imports, then initialises a snarkVM process. Doing that once per request is wasteful when a service handles many calls against the same program.

typescript
const preparedProgram = await programManager.prepareProgram({
    programName: "token.aleo",
    functionName: "transfer_public",
});

try {
    const authorization = await programManager.buildAuthorization({
        programName: "token.aleo",
        functionName: "transfer_public",
        inputs: ["aleo1receiver...", "100u64"],
        preparedProgram,
    });
    // reuse preparedProgram across further calls
} finally {
    preparedProgram.free();
}

prepareProgram fails immediately if the function does not exist, which turns a late proving-time error into an early one. Reuse validates that the program and function still match, so a stale context cannot silently sign for the wrong thing.

Two constraints worth respecting. Calls sharing a context must be sequential, so do not hand one context to concurrent workers. And the context is caller-owned with no global cache or invalidation policy behind it, so free() is your responsibility: hold one per hot program for the life of the process and release it on shutdown.

10. Delegated proving

Proof generation is the expensive part. A service on modest infrastructure can build the authorization locally and let a remote prover do the work.

typescript
// 1. Build the authorization locally: signing, not proving
const authorization = await programManager.buildAuthorization({
    programName: "token.aleo",
    functionName: "transfer_public",
    inputs: ["aleo1receiver...", "100u64"],
});

// 2. Hand it to a proving service
const response = await fetch(provingServiceUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ authorization: authorization.toString() }),
});
const completeTx = await response.json();

// 3. Broadcast the result
const txId = await networkClient.submitTransaction(completeTx);

provingRequest() packages this flow when the prover speaks the SDK's request format, and accepts the same preparedProgram option.

An authorization is a signed commitment to execute a specific function with specific inputs. The prover cannot alter what it authorizes, but it does see the inputs, so anything private in that call is disclosed to whoever proves it. Run your own prover when the inputs matter.

11. Key caching

typescript
const keyProvider = new AleoKeyProvider();
keyProvider.useCache(true);

Proving keys are large and synthesising them is slow. Cache them before you take production traffic, and use LocalFileKeyStore from the Node entry point to persist them across restarts rather than paying the synthesis cost on every deploy.

12. Environment

bash
ALEO_PRIVATE_KEY=APrivateKey1...     # never log this
ALEO_VIEW_KEY=AViewKey1...           # safe for read-only services
ALEO_NETWORK=testnet
ALEO_ENDPOINT=https://api.explorer.provable.com/v1

13. Security notes

Keep keys in a secrets manager, never in source and never in logs. Use view keys for anything that only reads; a monitoring service holding a private key is an unnecessary liability.

Separate signing from orchestration. The service that decides what to do and the service that can sign do not need to be the same process, and keeping them apart limits what a compromise of the busier one gets you.

Use an HSM or a secure enclave for private key operations in production.

Remember that a view key cannot be revoked and covers every record its address ever received. Use a separate address per relationship if you need to disclose selectively.

14. Performance notes

Reuse one long-lived AleoNetworkClient per process.

Cache proving keys with useCache(true) and persist them with LocalFileKeyStore.

Use prepareProgram() for hot programs so import resolution and process setup happen once rather than per request.

Back off exponentially when polling. Confirmation polling at a fixed short interval is the fastest way to get throttled.

Delegate proving when the service runs on CPU-limited infrastructure and the inputs are not sensitive.

15. Common backend errors

ErrorCauseFix
"Record not found"no unspent record to pay a private feeFund the account, or use privateFee: false
"Insufficient fee"priority fee too low under loadRaise priorityFee; the base fee is automatic
"Program not found"wrong program ID or wrong networkCheck the ID and that the endpoint matches the network
"Invalid input format"missing Leo type suffixUse "100u64", not "100"
Fee option appears to be ignoredpassing fee or baseFeeUse priorityFee; baseFee is deprecated and ignored
Deployment builder rejects its argumentoptions object passed positionallybuildDeploymentTransaction(source, priorityFee, privateFee)
Timeout during provinglocal proof generation too slowDelegate proving, or raise the timeout
Authorization rejected by provermismatched program, function, or inputsRebuild with the exact values the prover expects
Stale prepared contextprogram or function changed under a cached contextCall free() and prepare again
Transaction confirmed but state unchangedthe final block was rejected on-chainRead the mapping back; check for a failed assertion or a get on a missing key

16. Production patterns

A read-only service holds a view key and nothing else:

typescript
const viewKey = ViewKey.from_string(process.env.ALEO_VIEW_KEY!);
const networkClient = new AleoNetworkClient(process.env.ALEO_ENDPOINT!);

const balance = await networkClient.getProgramMappingValue(
    "token.aleo",
    "account",
    targetAddress,
);

A transaction service with bounded retries:

typescript
async function executeWithRetry(
    pm: ProgramManager,
    client: AleoNetworkClient,
    programName: string,
    functionName: string,
    inputs: string[],
    priorityFee: number,
    maxRetries = 3,
): Promise<string> {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            const tx = await pm.buildExecutionTransaction({
                programName,
                functionName,
                inputs,
                priorityFee,
                privateFee: false,
            });
            const txId = await client.submitTransaction(tx);
            await waitForConfirmation(client, txId);
            return txId;
        } catch (error) {
            if (attempt === maxRetries - 1) throw error;
            await new Promise((r) => setTimeout(r, 2000 * 2 ** attempt));
        }
    }
    throw new Error("unreachable");
}

Retries need care here. A transaction that was broadcast and then timed out during confirmation may still land, so retrying blindly can double-spend the intent. Check for the original transaction ID before resubmitting.

Write Leo programs with aleo_smart_contracts. Browser integration with aleo_frontend. Deployment with aleo_deployment. Staking with aleo_staking_delegation. Working code in aleo_cookbook.

18. Agent backend workflow

  1. Model the operation: program ID, function name, input types, and the state change you expect.
  2. Validate inputs before calling the SDK: Leo type suffixes and address shape.
  3. Estimate the fee with estimateExecutionFee when cost matters.
  4. Build the transaction or authorization with an explicit priorityFee and privateFee.
  5. Broadcast through one client path so parallel workers cannot double-submit.
  6. Poll for confirmation with bounded retries and exponential backoff.
  7. Verify the resulting state with a mapping read before reporting success.
  8. On failure, match the error to the table in section 15 and apply that fix. Retry only where a retry is safe.

Sources