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
npm install @provablehq/sdk@0.11.6
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
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:
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
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:
const estimate = await programManager.estimateExecutionFee({
programName: "token.aleo",
functionName: "transfer_public",
});
console.log("Estimated fee (microcredits):", estimate);
6. Waiting for confirmation
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
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:
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:
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
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.
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.
// 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
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
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
| Error | Cause | Fix |
|---|---|---|
| "Record not found" | no unspent record to pay a private fee | Fund the account, or use privateFee: false |
| "Insufficient fee" | priority fee too low under load | Raise priorityFee; the base fee is automatic |
| "Program not found" | wrong program ID or wrong network | Check the ID and that the endpoint matches the network |
| "Invalid input format" | missing Leo type suffix | Use "100u64", not "100" |
| Fee option appears to be ignored | passing fee or baseFee | Use priorityFee; baseFee is deprecated and ignored |
| Deployment builder rejects its argument | options object passed positionally | buildDeploymentTransaction(source, priorityFee, privateFee) |
| Timeout during proving | local proof generation too slow | Delegate proving, or raise the timeout |
| Authorization rejected by prover | mismatched program, function, or inputs | Rebuild with the exact values the prover expects |
| Stale prepared context | program or function changed under a cached context | Call free() and prepare again |
| Transaction confirmed but state unchanged | the final block was rejected on-chain | Read 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:
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:
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
- Model the operation: program ID, function name, input types, and the state change you expect.
- Validate inputs before calling the SDK: Leo type suffixes and address shape.
- Estimate the fee with
estimateExecutionFeewhen cost matters. - Build the transaction or authorization with an explicit
priorityFeeandprivateFee. - Broadcast through one client path so parallel workers cannot double-submit.
- Poll for confirmation with bounded retries and exponential backoff.
- Verify the resulting state with a mapping read before reporting success.
- On failure, match the error to the table in section 15 and apply that fix. Retry only where a retry is safe.