Skip to main content

Overview

Brale is a stablecoin platform that provides regulated stablecoin issuance, compliance, reserve management, custody, on and offramps, and transfers through a unified API. In this guide we’ll show how to:
  • Use Turnkey to provision a Base EVM address, then constrain signing with a Turnkey policy.
  • Register that address in Brale as an Address.
  • Use Brale to fund the address via an inbound USD Automation.
  • Use Brale to send stablecoins out via a direct Transfer.

Getting started

Prerequisites

  • A Turnkey organization with:
    • A root user API key (admin operations)
    • A non-root user API key (day-to-day signing)
    • An EVM wallet with a Base address, funded with a small amount of ETH for gas
  • Brale API credentials (client_id and client_secret). See Authentication.
  • Brale sandbox or testnet first, then mainnet. See Sandbox and testnet.
  • A stablecoin and chain to test with, for example SBC on Base. See transfer_type and value_type.

Environment variables

You will reference these throughout.
# Turnkey
export TURNKEY_ORGANIZATION_ID="..."
export TURNKEY_BASE_URL="https://api.turnkey.com"

# Root user key, only for admin actions like creating policies
export TURNKEY_ROOT_API_PUBLIC_KEY="..."
export TURNKEY_ROOT_API_PRIVATE_KEY="..."

# Non-root user key, use this for signing so policies are enforced
export TURNKEY_NONROOT_API_PUBLIC_KEY="..."
export TURNKEY_NONROOT_API_PRIVATE_KEY="..."

# Wallet identifier inside your Turnkey org
export TURNKEY_SIGN_WITH="..."

# Base
export BASE_RPC_URL="..."

# Brale
export BRALE_CLIENT_ID="..."
export BRALE_CLIENT_SECRET="..."

Set up a constrained Turnkey signer (EVM, Base)

Create a policy for the non-root user

Use the root API key to create a policy, but use the non-root API key for signing transactions so the policy engine is enforced. This example policy restricts the non-root user to sending transactions only to a small allowlist of contracts. Replace the allowlist with whatever your application needs.
import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";

const rootClient = new TurnkeyServerSDK({
  apiBaseUrl: process.env.TURNKEY_BASE_URL!,
  apiPrivateKey: process.env.TURNKEY_ROOT_API_PRIVATE_KEY!,
  apiPublicKey: process.env.TURNKEY_ROOT_API_PUBLIC_KEY!,
  defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
}).apiClient();

const nonRootUserId = "<TURNKEY_NONROOT_USER_ID>";

const ALLOWLIST = [
  "0x0000000000000000000000000000000000000000", // replace
];

const { policyId } = await rootClient.createPolicy({
  policyName: "Restrict non-root signing",
  effect: "EFFECT_ALLOW",
  consensus: `approvers.any(user, user.id == '${nonRootUserId}')`,
  condition: `eth.tx.to in ${JSON.stringify(ALLOWLIST)}`,
  notes: "Restrict signing to an allowlist of contracts",
});

console.log("Created policy", policyId);

Create a viem account backed by Turnkey

import { createAccount } from "@turnkey/viem";
import { Turnkey as TurnkeyServerSDK } from "@turnkey/sdk-server";
import { base } from "viem/chains";
import { createPublicClient, createWalletClient, http, type Account } from "viem";

const nonRootClient = new TurnkeyServerSDK({
  apiBaseUrl: process.env.TURNKEY_BASE_URL!,
  apiPrivateKey: process.env.TURNKEY_NONROOT_API_PRIVATE_KEY!,
  apiPublicKey: process.env.TURNKEY_NONROOT_API_PUBLIC_KEY!,
  defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID!,
});

const turnkeyAccount = await createAccount({
  client: nonRootClient.apiClient(),
  organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
  signWith: process.env.TURNKEY_SIGN_WITH!,
});

const publicClient = createPublicClient({
  chain: base,
  transport: http(process.env.BASE_RPC_URL!),
});

const walletClient = createWalletClient({
  chain: base,
  account: turnkeyAccount as Account,
  transport: http(process.env.BASE_RPC_URL!),
});

const turnkeyEvmAddress = (turnkeyAccount as Account).address;
console.log("Turnkey EVM address", turnkeyEvmAddress);

Authenticate to Brale

Brale uses a bearer token obtained from client_id and client_secret. See Authentication. Store the resulting token as BRALE_ACCESS_TOKEN.

Create a Brale Account

An Account represents your customer (end user or business) in Brale. See Accounts. Capture ACCOUNT_ID.

Register the Turnkey EVM address as a Brale Address

In Brale, an Address is the universal source and destination primitive for onchain and offchain endpoints. See Addresses. Create an Address for turnkeyEvmAddress on Base, capture TURNKEY_ADDRESS_ID.

Inbound USD funding via Brale Automations (event-driven)

Automations provision a unique set of USD funding instructions. When USD arrives, Brale automatically creates a Transfer to mint and send stablecoins to your configured destination address. See Automations.

Create an Automation

Create an automation that mints SBC on Base to your Turnkey address.
curl -s -X POST "https://api.brale.xyz/accounts/${ACCOUNT_ID}/automations" \
  -H "Authorization: Bearer ${BRALE_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Turnkey Base Onramp",
    "source": { "value_type": "USD" },
    "destination": {
      "address_id": "'"${TURNKEY_ADDRESS_ID}"'",
      "value_type": "SBC",
      "transfer_type": "base"
    }
  }'

Fetch funding instructions

Once the automation is active, Brale populates source.funding_instructions.
curl -s "https://api.brale.xyz/accounts/${ACCOUNT_ID}/automations" \
  -H "Authorization: Bearer ${BRALE_ACCESS_TOKEN}"

Reconcile inbound activity via Transfers

When USD hits the automation’s virtual account, Brale will create a Transfer automatically.

Outbound stablecoin payout via Transfers (API-driven)

A Transfer moves value between fiat and stablecoins, between stablecoins, and to external addresses. See Transfers.

Register the recipient address

Create a second Brale Address for the external recipient on Base, capture RECIPIENT_ADDRESS_ID.

Create a payout transfer

This example sends SBC on Base from your Turnkey address to the external recipient.
curl -s -X POST "https://api.brale.xyz/accounts/${ACCOUNT_ID}/transfers" \
  -H "Authorization: Bearer ${BRALE_ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount": { "value": "10", "currency": "USD" },
    "source": {
      "address_id": "'"${TURNKEY_ADDRESS_ID}"'",
      "value_type": "SBC",
      "transfer_type": "base"
    },
    "destination": {
      "address_id": "'"${RECIPIENT_ADDRESS_ID}"'",
      "value_type": "SBC",
      "transfer_type": "base"
    }
  }'
Capture TRANSFER_ID from the response.

Retrieve the transfer until terminal

Use GET to retrieve a single transfer and check status (pending, processing, complete, failed).
curl -s "https://api.brale.xyz/accounts/${ACCOUNT_ID}/transfers/${TRANSFER_ID}" \
  -H "Authorization: Bearer ${BRALE_ACCESS_TOKEN}"

List transfers for reconciliation

curl -s "https://api.brale.xyz/accounts/${ACCOUNT_ID}/transfers?value_type=SBC&transfer_type=base&page[size]=10" \
  -H "Authorization: Bearer ${BRALE_ACCESS_TOKEN}"

Security considerations

  • Use Turnkey non-root users plus policies for least-privilege signing.
  • Never ship Brale client_secret to browsers.
  • Always include Idempotency-Key on Brale create POSTs, and reuse the same key when retrying the same logical operation.