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

# Deposit from Stellar to Tempo

> Create a Tempo Routes deposit address on Stellar and fund it with USDC from a Privy Stellar wallet

[Tempo Routes](https://tempo.xyz/developers/docs/routes) moves stablecoins between Tempo and other networks. A deposit address is a Routes address on another network. Funds sent to it are delivered to a chosen recipient on Tempo. Developers building on [Stellar](https://stellar.org/) can use this recipe to move USDC on Stellar to OUSD on Tempo.

This recipe creates a deposit address on Stellar that delivers USDC to Tempo as OUSD. It then funds the address with USDC from a Privy Stellar wallet and tracks the deposit until it reaches Tempo. As an option, Tempo Routes can subsidize the route cost so that 1 USDC on Stellar delivers 1 OUSD on Tempo. The subsidy requires approval for your organization. See [Quote the deposit](#1-quote-the-deposit).

## Prerequisites

> [!WARNING]
>
> This recipe runs on Stellar mainnet and Tempo mainnet only, because Tempo Routes lists no Stellar
> testnet route. Every step moves real USDC and XLM.

Before starting, set up the following:

* A Privy app and its app secret. The signing step shows the [Node.js SDK](https://docs.privy.io/basics/nodeJS/setup.md), the [Python SDK](https://docs.privy.io/basics/python/setup.md), and the REST API.
* A Privy wallet with the `stellar` chain type that sends the USDC. It also receives refunds if a deposit can't be delivered. See [Create a wallet](https://docs.privy.io/wallets/wallets/create/create-a-wallet.md) to create one.
* A Tempo address that receives the OUSD. Tempo is EVM-compatible, so a Privy wallet with the `ethereum` chain type works.
* A Tempo API key with the `routes:read` and `routes:write` scopes. To get a Tempo API key for your app, contact [Privy support](mailto:support@privy.io).
* A Stellar SDK to build and submit Stellar transactions, such as [`@stellar/stellar-sdk`](https://www.npmjs.com/package/@stellar/stellar-sdk) for Node.js or [`stellar-sdk`](https://pypi.org/project/stellar-sdk/) for Python

Keep the Privy app secret and the Tempo API key on the server. Show users only the deposit address and its funding instructions.

Steps 1, 2, and 4 call the Tempo Routes API at `https://api.tempo.xyz` with the Tempo API key. These are plain HTTP requests, so any language works. Step 3 calls the Privy API to sign the Stellar transaction.

### The route

This recipe uses the following Stellar to Tempo route:

| Field | Value |
| - | - |
| Route | `stellar:pubnet/usdc-eip155:4217/ousd` |
| Source chain | Stellar mainnet, `stellar:pubnet` |
| Source token | USDC issued by `GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN`, 7 decimals |
| Destination chain | Tempo mainnet, `eip155:4217` |
| Destination token | OUSD, `0x20c0000000000000000000006a37da5c996874be`, 6 decimals |
| Amount mode | Exact source: the app fixes the amount sent on Stellar |

Tempo Routes identifies the source token as `USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN`. The Tempo Routes public directory lists the currently supported Stellar routes at `https://api.tempo.xyz/v1/routes?sourceChain=stellar:pubnet`. Other destination tokens on Tempo, such as USDC.e, use the same flow with a different `destinationToken`.

Before it can send USDC, the Stellar wallet needs:

* XLM for the account's minimum balance and network fees
* A trustline to USDC from the issuer in [the route](#the-route)
* A USDC balance

A Stellar account must hold a minimum balance of XLM. The minimum is currently 1 XLM for the account, plus 0.5 XLM for each trustline. Without a USDC trustline, the wallet can't hold USDC, and a USDC payment fails with `op_src_no_trust`. Keep the trustline in place after the deposit: the wallet needs it to receive a USDC refund.

Add the trustline once per wallet. Build a transaction with a change-trust operation for USDC, then sign and submit it the same way as the payment in [step 3](#3-send-usdc-to-the-deposit-address).

**NodeJS**

```typescript theme={"system"}
import {
  Asset,
  BASE_FEE,
  Horizon,
  Networks,
  Operation,
  TransactionBuilder
} from '@stellar/stellar-sdk';

const walletAddress = 'GStellarWalletAddress';
const usdc = new Asset('USDC', 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN');

const server = new Horizon.Server('https://horizon.stellar.org');
const account = await server.loadAccount(walletAddress);
const transaction = new TransactionBuilder(account, {
  fee: BASE_FEE,
  networkPassphrase: Networks.PUBLIC
})
  .addOperation(Operation.changeTrust({asset: usdc}))
  .setTimeout(180)
  .build();
```

**Python**

```python theme={"system"}
from stellar_sdk import Asset, Network, Server, TransactionBuilder

wallet_address = "GStellarWalletAddress"
usdc = Asset("USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")

server = Server("https://horizon.stellar.org")
account = server.load_account(wallet_address)
transaction = (
    TransactionBuilder(
        source_account=account,
        network_passphrase=Network.PUBLIC_NETWORK_PASSPHRASE,
        base_fee=100,
    )
    .append_change_trust_op(asset=usdc)
    .set_timeout(180)
    .build()
)
```

**REST API**

Build the change-trust transaction with any Stellar SDK. Then sign its hash and submit it as in [step 3](#3-send-usdc-to-the-deposit-address).

> [!NOTE]
>
> This recipe uses wallets owned by the app, so the app secret alone authorizes signing. For a
> wallet with an owner, the signing request also needs an authorization signature. See
> [Authorization context](https://docs.privy.io/controls/authorization-context) for each SDK, and [Create a
> wallet](https://docs.privy.io/wallets/wallets/create/create-a-wallet.md) for wallet ownership.

## 1. Quote the deposit

Request a quote from the Tempo Routes API with [`GET https://api.tempo.xyz/v1/routes/deposit-addresses/quote`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#quoteroutesdepositaddress). Set `amount` in USDC base units. Stellar USDC has 7 decimals, so `10000000` is 1 USDC. Pass the recipient and the refund address that the deposit address will use.

```typescript theme={"system"}
const TEMPO_API_KEY = process.env.TEMPO_API_KEY!;
const USDC_ISSUER = 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN';

async function quoteDeposit({
  recipient,
  refundAddress,
  amount
}: {
  recipient: string; // Tempo wallet address
  refundAddress: string; // Stellar wallet address
  amount: string; // USDC base units, e.g. '10000000' for 1 USDC
}) {
  const query = new URLSearchParams({
    sourceChain: 'stellar:pubnet',
    sourceToken: `USDC-${USDC_ISSUER}`,
    destinationToken: 'ousd',
    recipient,
    refundAddress,
    amount,
    subsidize: 'false'
  });

  const response = await fetch(`https://api.tempo.xyz/v1/routes/deposit-addresses/quote?${query}`, {
    headers: {'tempo-api-key': TEMPO_API_KEY}
  });
  if (!response.ok) {
    throw new Error(`Tempo Routes returned ${response.status}: ${await response.text()}`);
  }
  const quote = await response.json();

  console.log('Send:', quote.sourceAmount.formatted, 'USDC');
  console.log('Receive:', quote.destinationAmount.formatted, 'OUSD');
  console.log('Fees:', quote.fees);
  console.log('Expires at:', quote.quote.expiresAt);
  return quote;
}
```

Show users the destination amount and fees from the quote. When `quote.quote.expiresAt` passes, request a new quote. A quote prices the amount; it doesn't mean funds arrived.

> [!TIP]
>
> Set `subsidize` to `true` for normalized 1:1 delivery, where Tempo funds the route cost. Tempo
> returns a `403` error when the organization is not eligible for a Routes subsidy. See [Quotes and
> fees](https://tempo.xyz/developers/docs/routes/quotes) for details. To enable subsidies for an
> app, contact [Privy support](mailto:support@privy.io).

## 2. Create the deposit address

Create the address with the Tempo Routes API: [`POST https://api.tempo.xyz/v2/routes/deposit-addresses`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#createroutesdepositaddressv2). Set `refundAddresses.stellar` to the Stellar wallet that sends the funds.

Send an `idempotency-key` header. Create one key for the user's address and store it with the user record before the request. The key protects retries of the same request:

* A retry with the same key and the same body returns the same address instead of creating another. Tempo replays the successful response for 24 hours.
* A request with the same key and a changed body, such as a new `amount`, returns a conflict.
* A failed request releases the key, so a retry with the same key can succeed.

After the address is created, store its ID. Read the address back later with [`GET https://api.tempo.xyz/v2/routes/deposit-addresses/{id}`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#getroutesdepositaddressv2) instead of creating it again.

```typescript theme={"system"}
const TEMPO_API_KEY = process.env.TEMPO_API_KEY!;
const USDC_ISSUER = 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN';

async function createDepositAddress({
  recipient,
  refundAddress,
  amount,
  idempotencyKey
}: {
  recipient: string; // Tempo wallet address
  refundAddress: string; // Stellar wallet address
  amount: string; // USDC base units
  idempotencyKey: string; // stored with the user record
}) {
  const response = await fetch('https://api.tempo.xyz/v2/routes/deposit-addresses', {
    method: 'POST',
    headers: {
      'tempo-api-key': TEMPO_API_KEY,
      'idempotency-key': idempotencyKey,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      sourceChain: 'stellar:pubnet',
      sourceToken: `USDC-${USDC_ISSUER}`,
      destinationToken: 'ousd',
      recipient,
      refundAddresses: {stellar: refundAddress},
      amount,
      subsidize: false
    })
  });
  if (!response.ok) {
    throw new Error(`Tempo Routes returned ${response.status}: ${await response.text()}`);
  }
  const depositAddress = await response.json();

  // When a route's terms change, `addresses` can list more than one Stellar address. Use the active one.
  const stellarEntry = depositAddress.addresses.find(
    (entry: {family: string; status: string}) =>
      entry.family === 'stellar' && entry.status === 'active'
  );
  if (!stellarEntry) {
    throw new Error('The deposit address cannot accept deposits yet');
  }

  console.log('Deposit address ID:', depositAddress.id);
  console.log('Send USDC on Stellar to:', stellarEntry.address);
  return {id: depositAddress.id, address: stellarEntry.address, addressId: stellarEntry.id};
}
```

The response lists one entry in `addresses` for each chain family. Save the top-level `id`, the Stellar entry's `address`, and its `id` (`rda_…`) with the user record. Invite deposits only to the entry whose `status` is `active`. On Stellar, the deposit `address` is a muxed account address that starts with `M`: a Tempo account plus an ID that identifies this deposit address, so the payment needs no memo. Pay it as it is; Stellar SDKs accept a muxed address as a payment destination.

## 3. Send USDC to the deposit address

Build a Stellar payment with a Stellar SDK. Privy signs the transaction hash with the Stellar wallet's Ed25519 key through [`raw_sign`](https://docs.privy.io/wallets/using-wallets/other-chains). Attach the signature to the transaction, then submit it to Horizon.

**NodeJS**

```typescript theme={"system"}
import {PrivyClient} from '@privy-io/node';
import {
  Asset,
  BASE_FEE,
  Horizon,
  Networks,
  Operation,
  TransactionBuilder
} from '@stellar/stellar-sdk';

const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!
});

const walletId = 'insert-stellar-wallet-id';
const walletAddress = 'GStellarWalletAddress';
const depositAddress = 'MDepositAddressFromStep2';
const usdc = new Asset('USDC', 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN');

// Build the payment. Stellar amounts are decimal strings with up to 7 decimals.
const server = new Horizon.Server('https://horizon.stellar.org');
const account = await server.loadAccount(walletAddress);
const transaction = new TransactionBuilder(account, {
  fee: BASE_FEE,
  networkPassphrase: Networks.PUBLIC
})
  .addOperation(Operation.payment({destination: depositAddress, asset: usdc, amount: '1.0000000'}))
  .setTimeout(180)
  .build();

// Sign the transaction hash with Privy.
const {signature} = await privy.wallets().rawSign(walletId, {
  params: {hash: `0x${Buffer.from(transaction.hash()).toString('hex')}`}
});

// Attach the signature and submit the transaction.
transaction.addSignature(walletAddress, Buffer.from(signature.slice(2), 'hex').toString('base64'));
const result = await server.submitTransaction(transaction);

console.log('Stellar transaction:', result.hash);
```

**Python**

```python theme={"system"}
import os

from privy import PrivyClient
from stellar_sdk import Asset, DecoratedSignature, Keypair, Network, Server, TransactionBuilder

client = PrivyClient(
    app_id=os.environ["PRIVY_APP_ID"],
    app_secret=os.environ["PRIVY_APP_SECRET"],
)

wallet_id = "insert-stellar-wallet-id"
wallet_address = "GStellarWalletAddress"
deposit_address = "MDepositAddressFromStep2"
usdc = Asset("USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")

# Build the payment. Stellar amounts are decimal strings with up to 7 decimals.
server = Server("https://horizon.stellar.org")
account = server.load_account(wallet_address)
transaction = (
    TransactionBuilder(
        source_account=account,
        network_passphrase=Network.PUBLIC_NETWORK_PASSPHRASE,
        base_fee=100,
    )
    .append_payment_op(destination=deposit_address, asset=usdc, amount="1.0000000")
    .set_timeout(180)
    .build()
)

# Sign the transaction hash with Privy.
response = client.wallets.raw_sign(
    wallet_id,
    wallet_raw_sign_params={"params": {"hash": f"0x{transaction.hash().hex()}"}},
)

# Attach the signature and submit the transaction.
signature = bytes.fromhex(response.data.signature.removeprefix("0x"))
signature_hint = Keypair.from_public_key(wallet_address).signature_hint()
transaction.signatures.append(DecoratedSignature(signature_hint, signature))
result = server.submit_transaction(transaction)

print("Stellar transaction:", result["hash"])
```

**REST API**

Build the payment with any Stellar SDK and compute its transaction hash. Then sign the hash with a `POST` request to:

```bash theme={"system"}
https://api.privy.io/v1/wallets/<wallet_id>/raw_sign
```

```bash theme={"system"}
curl --request POST https://api.privy.io/v1/wallets/<wallet_id>/raw_sign \
  -u "<your-privy-app-id>:<your-privy-app-secret>" \
  -H "privy-app-id: <your-privy-app-id>" \
  -H 'Content-Type: application/json' \
  -d '{
    "params": {
      "hash": "0x<Stellar transaction hash>"
    }
  }'
```

A successful response looks like:

```json theme={"system"}
{
  "method": "raw_sign",
  "data": {
    "signature": "0x<Ed25519 signature>",
    "encoding": "hex"
  }
}
```

Remove the `0x` prefix and attach the signature to the transaction with the wallet's signature hint. Then submit the signed transaction envelope to Horizon with `POST https://horizon.stellar.org/transactions`, in the `tx` form field as base64 XDR.

Send the amount from the quote. Stellar payments take a decimal amount, so the quoted `10000000` base units are `1.0000000` USDC. The deposit address accepts each new deposit on its route, so users can fund it more than once.

## 4. Track the deposit

List the deposits the address received with the Tempo Routes API: [`GET https://api.tempo.xyz/v1/routes/deposits`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#listroutesdeposits). Find the deposit that matches the Stellar transaction, then poll it until it completes, is refunded, or needs action. Set `depositAddress` to the Stellar address, not the `rda_…` ID. To filter by ID, use `depositAddressId` instead.

```typescript theme={"system"}
const TEMPO_API_KEY = process.env.TEMPO_API_KEY!;
// `action-required` is not final, but the deposit needs attention before it can move on.
const STOP_POLLING_STATUSES = ['completed', 'refunded', 'action-required'];
const POLL_INTERVAL_MS = 10_000;
const POLL_TIMEOUT_MS = 15 * 60_000;

async function getTempo(path: string) {
  const response = await fetch(`https://api.tempo.xyz${path}`, {
    headers: {'tempo-api-key': TEMPO_API_KEY}
  });
  if (!response.ok) {
    throw new Error(`Tempo Routes returned ${response.status}: ${await response.text()}`);
  }
  return response.json();
}

type DepositSearch = {
  depositAddress: string; // Stellar deposit address from step 2
  transactionHash: string; // Stellar transaction hash from step 3
};

// Reads every page of the address's deposits. Returns undefined until Tempo detects the transaction.
async function findDeposit({depositAddress, transactionHash}: DepositSearch) {
  let cursor: string | null = null;
  do {
    const query = new URLSearchParams({depositAddress, limit: '50'});
    if (cursor) query.set('cursor', cursor);
    const page = await getTempo(`/v1/routes/deposits?${query}`);
    const deposit = page.data.find((item: {sourceTransactionHashes: string[]}) =>
      item.sourceTransactionHashes.includes(transactionHash)
    );
    if (deposit) return deposit;
    cursor = page.nextCursor;
  } while (cursor);
  return undefined;
}

async function waitForDeposit(search: DepositSearch) {
  const deadline = Date.now() + POLL_TIMEOUT_MS;
  let deposit = await findDeposit(search);
  while (!deposit || !STOP_POLLING_STATUSES.includes(deposit.status)) {
    if (Date.now() > deadline) {
      throw new Error(
        `Deposit still ${deposit?.status ?? 'not detected'} after ${POLL_TIMEOUT_MS} ms`
      );
    }
    console.log('Deposit status:', deposit?.status ?? 'not detected yet');
    await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
    deposit = deposit
      ? await getTempo(`/v1/routes/deposits/${deposit.id}`)
      : await findDeposit(search);
  }
  console.log('Deposit:', deposit.id, deposit.status);
  return deposit;
}
```

The list returns deposits in `data`, newest first, and `nextCursor` points to the next page. `findDeposit` follows `nextCursor` until it finds the transaction, so it also works for an address with many deposits. It returns `undefined` until Tempo detects the Stellar transaction, and `waitForDeposit` keeps searching until then. `waitForDeposit` throws after `POLL_TIMEOUT_MS`, for example when the payment never reached the deposit address. After the deposit is found, `waitForDeposit` reads only that deposit with [`GET https://api.tempo.xyz/v1/routes/deposits/{id}`](https://tempo.xyz/developers/docs/api/routes/deposit-addresses#getroutesdeposit) and its `rdp_…` ID.

A deposit moves through `detected`, `bridging`, and `settling` to `completed`. It can also move to `action-required`, `refunding`, or `refunded`.

## Reconcile delivery and refunds

Save the user, the deposit address ID, and each deposit ID together. One address can receive many deposits, so reconcile each deposit on its own:

* **`completed`**: The deposit reached Tempo. `destinationAmount` is the OUSD delivered, and `destinationTransactionHashes` lists the Tempo transactions.
* **`refunding`** then **`refunded`**: Tempo returns the funds to the refund address on Stellar. `refundAmount` and `refundTransactionHashes` describe the refund.
* **`action-required`**: The deposit needs attention. `statusReason.code` and `statusReason.message` explain why.

Only a `completed` deposit confirms delivery to Tempo. An `active` address only means the address can receive funds. Before crediting a balance or treating a refund as complete, follow [Track delivery](https://tempo.xyz/developers/docs/routes/delivery).

## Learn more

* [Accept deposits](https://tempo.xyz/developers/docs/routes/deposits) in the Tempo Routes documentation
* [Deposit addresses API reference](https://tempo.xyz/developers/docs/api/routes/deposit-addresses)
* [Using Tempo with Privy](https://docs.privy.io/recipes/tempo/send-transactions.md)
* [Tier 2 wallet integration](https://docs.privy.io/recipes/tier-2-wallet-integration.md)
