Quickstart

Give your users a private balance funded in local currency, with a few HTTP calls.

This is the flow Shield's own web follows. You need the stellar-private-payments browser SDK (0.2.1) for the keys, and Freighter or any SEP-43 wallet for the signatures.

1. Read the deployment

const info = await fetch(`${SHIELD}/v1/info`).then((r) => r.json());
// info.network_passphrase, info.pool.contract_id, info.modes.ramp.enabled

2. Derive the user's pool keys

The wallet signs once to derive them. They stay in the browser.

import init, { Client, Storage } from "stellar-private-payments";

await init();
const client = await Client.new({ rpcUrl, storage: await Storage.open(), contractConfig, circuitsBaseUrl, circuitLock });
const account = await client.account({ networkPassphrase, userAddress }, signer); // e.g. Freighter
const keys = await account.derivePrivacyKeys(); // the wallet signs once
const aspLeaf = await account.deriveAspUserLeaf();

3. Onboard with a signature

Sign this exact text with the account (SEP-53, Freighter's signMessage), with keys in lowercase hex. Without it, 400 invalid_signature.

const message = [
  "Lendara Shield onboarding",
  "network: testnet",
  `account: ${userAddress}`,
  `note_public_key: ${keys.notePublicKey.toLowerCase()}`,
  `encryption_public_key: ${keys.encryptionPublicKey.toLowerCase()}`,
  `asp_leaf: ${aspLeaf.toLowerCase()}`,
].join("\n");
const { signedMessage } = await signMessage(message, { address: userAddress, networkPassphrase });

await fetch(`${SHIELD}/v1/onboarding`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    stellar_account: userAddress,
    note_public_key: keys.notePublicKey.toLowerCase(),
    encryption_public_key: keys.encryptionPublicKey.toLowerCase(),
    asp_leaf: aspLeaf.toLowerCase(),
    signature: signedMessage,
    scenario: "approved", // sandbox only
  }),
});

Poll GET /v1/users/{account} while asp_status is pending, until it is allowed.

4. Create a ramp intent

// Only the account's owner can start a deposit: it signs the amount and a single-use id.
const request = { fiat_currency: "CRC", amount: "5000", request_id: crypto.randomUUID(), issued_at: Math.floor(Date.now() / 1000) };
const text = [
  "Lendara Shield intent",
  "network: testnet",
  `account: ${userAddress}`,
  `fiat_currency: ${request.fiat_currency}`,
  `amount: ${request.amount}`,
  `request_id: ${request.request_id}`,
  `issued_at: ${request.issued_at}`,
].join("\n");
const signed = await signMessage(text, { address: userAddress, networkPassphrase });

const intent = await fetch(`${SHIELD}/v1/intents`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ mode: "ramp", stellar_account: userAddress, ...request, signature: signed.signedMessage }),
}).then((r) => r.json());

window.open(intent.interactive_url, "anchor", "popup"); // the anchor's SEP-24 widget

5. Follow it to completed

Poll every 2–3 seconds. The three hashes appear as each step lands on-chain.

const FINAL = ["completed", "rejected", "expired", "error"];
let current = intent;
while (!FINAL.includes(current.status)) {
  await new Promise((r) => setTimeout(r, 2500));
  current = await fetch(`${SHIELD}/v1/intents/${intent.id}`).then((r) => r.json());
}
// current.transactions: { intake_payment, pool_deposit, pool_transfer }

6. Show the private balance

const pool = await account.pool({ poolContract: info.pool.contract_id });
await client.sync();
const stroops = await pool.balance(); // bigint, 7 decimals