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

# Power Yield for Stellar Balances

> Use Ground to offer yield on customer USDC held on Stellar. Ground provides the wallet, allocation, bridging, and settlement infrastructure while your application owns the customer experience.

<Note>Stellar Testnet is available in sandbox. Stellar mainnet is available in beta for enabled production environments.</Note>

## Configure your backend

Create an API key in [Ground Portal → API Keys](https://portal.groundtech.co/api-keys).
Copy it when shown and store it in your backend's secret manager as
`GROUND_API_TOKEN`. Never expose an API key in browser or mobile code. The
examples use sandbox:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const baseUrl = "https://sandbox.groundtech.co";
const apiToken = process.env.GROUND_API_TOKEN!;
const stellarChain = "stellar_testnet";
```

For production beta access, use `https://production.groundtech.co`, a production API key, and `stellar` as the chain key.

## 1. Create a wallet for the customer

Create one Portfolio Wallet for each customer. Store the Ground wallet ID on
the customer record in your database.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type Wallet = {
  id: string;
  status: "creating" | "idle" | "failed";
  depositAddresses: Record<string, string>;
  balance: {
    totalUsd: string;
    withdrawableUsd: string;
    earnedUsd: string;
  };
  positions: Array<{
    id: string;
    kind: string;
    label: string;
    valueUsd: string;
  }>;
};

async function createCustomerWallet(customerId: string) {
  const requestId = crypto.randomUUID();
  await saveGroundRequestId(customerId, "wallet_creation", requestId);

  const response = await fetch(`${baseUrl}/v2/wallets`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      requestId,
      label: `Customer ${customerId}`,
      autoRebalance: true,
      strategy: {
        allocations: {
          usdc: [
            {
              yieldSourceId: "your-usdc-yield-source-id",
              pct: 100,
            },
          ],
        },
      },
    }),
  });

  if (!response.ok) throw new Error(await response.text());
  const wallet = (await response.json()) as Wallet;

  await saveGroundWalletId(customerId, wallet.id);
  return wallet;
}

async function getCustomerWallet(walletId: string) {
  const response = await fetch(`${baseUrl}/v2/wallets/${walletId}`, {
    headers: { Authorization: `Bearer ${apiToken}` },
  });

  if (!response.ok) throw new Error(await response.text());
  return (await response.json()) as Wallet;
}

const customerWallet = await createCustomerWallet("customer-1234");
```

Replace `your-usdc-yield-source-id` with the USDC yield source configured for
your integration. Save the `requestId` before sending the request. If the
request times out, retry it with the same ID.

Wallet creation is asynchronous. Subscribe to
`portfolio_wallet.status_changed` before creating the wallet. Store the returned
wallet ID, then wait for its status to become `idle`. If a webhook is delayed or
missed, reconcile the state with `GET /v2/wallets/{id}`. Treat `failed` as a
provisioning failure.

Stellar will not appear in `depositAddresses` yet. Enable it separately after
the wallet is ready.

## 2. Enable Stellar deposits

### Configure approval signing

Configure an approval signer once for each Ground organization and environment
before enabling Stellar deposits. The platform controls this signer and uses it
to authorize the Stellar deposit setup.

For a server-side integration, configure a machine signer so your backend can
verify and approve activities automatically:

1. Open
   [Ground Portal → Settings → Signing](https://portal.groundtech.co/dashboard/settings?tab=policies)
   and enable approval signing.
2. Create and register a machine signing key.
3. Store the private key in your backend's secret manager. Never send it to
   Ground or expose it in client code.
4. When Ground creates an approval, verify the wallet, network, and activity
   details before stamping and submitting the vote from your signing service.

You can use a passkey signer when an internal operator should approve activities
manually. See
[Transaction Approvals](/docs/portfolio-wallets/transaction-approvals) for both
signer options and the complete stamping flow.

<Warning>
  Automatic signing must still verify every activity before approval. Do not
  blindly sign an activity received from a webhook or API response.
</Warning>

If approval signing is not configured, the enable endpoint returns `409` with
`stellar_customer_approval_unavailable`. Ground does not create a Stellar setup
or approval activity.

### Subscribe to approval events

Subscribe to `portfolio_wallet.approval.status_changed` before enabling Stellar
deposits so your application receives the approval created by the request. See
[Webhooks](/docs/portfolio-wallets/webhooks).

Verify the webhook signature before parsing it and deduplicate deliveries using
the `Ground-Event-Id` header.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type ApprovalWebhook = {
  event: "portfolio_wallet.approval.status_changed";
  walletId: string;
  approval: {
    activityId: string;
    activityKind: string;
    chain: string;
    status: "pending" | "approved" | "rejected" | "expired";
  };
};

async function handleApprovalEvent(event: ApprovalWebhook) {
  if (event.approval.activityKind !== "stellar_trustline") return;

  const customer = await findCustomerByGroundWalletId(event.walletId);

  if (event.approval.status === "pending") {
    await enqueueStellarSetupApproval({
      organizationId: customer.organizationId,
      walletId: event.walletId,
      activityId: event.approval.activityId,
      chain: event.approval.chain,
    });
    return;
  }

  await updateStellarEnablement(customer.id, event.approval.status);
}
```

The approval job should verify the activity and then submit it through the
machine signer flow below. For passkey signing, notify an internal operator
instead.

### Request Stellar setup

After the webhook subscription is active, call the enable endpoint from your
backend for the customer's saved wallet ID:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type StellarEnablement = {
  chain: "stellar_testnet" | "stellar";
  status: "provisioning" | "ready";
  address?: string;
};

async function enableStellarDeposits(walletId: string) {
  const response = await fetch(
    `${baseUrl}/v2/wallets/${walletId}/deposit-addresses/${stellarChain}/enable`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${apiToken}` },
    },
  );

  if (!response.ok) throw new Error(await response.text());
  return (await response.json()) as StellarEnablement;
}

await enableStellarDeposits(customerWallet.id);
```

Ground prepares the Stellar account and creates a transaction approval for the
Stellar deposit setup. This one-time approval allows the account to receive
USDC. Stellar calls this authorization a trustline. It does not move or allocate
customer funds.

### Approve the Stellar deposit setup

The platform backend requests the Turnkey approval payload, verifies the
activity, stamps it with the organization signer, and submits the vote:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function submitStellarSetupApproval(activityId: string) {
  const approvalResponse = await fetch(
    `${baseUrl}/v2/turnkey/activity-approval-request`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ activityId, action: "approve" }),
    },
  );
  if (!approvalResponse.ok) {
    throw new Error(await approvalResponse.text());
  }

  const approvalRequest = await approvalResponse.json();
  const approvalStamp = await approvalStamper.stamp(
    approvalRequest.stampPayload,
  );

  const voteResponse = await fetch(
    `${baseUrl}/v2/turnkey/activities/${activityId}/vote`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        action: "approve",
        turnkeyRequest: approvalRequest.turnkeyRequest,
        customerApprovalStamp: approvalStamp,
      }),
    },
  );
  if (!voteResponse.ok) throw new Error(await voteResponse.text());
}
```

Before stamping, verify that the pending activity matches the expected wallet,
`stellar_trustline` activity kind, and Stellar network. See
[Transaction Approvals](/docs/portfolio-wallets/transaction-approvals) for
the complete verification flow. Continue after
`portfolio_wallet.approval.status_changed` reports `approved`.

If the approval is rejected or expires, call the enable endpoint again. Ping
Ground if the error persists.

After the approval succeeds, the Stellar deposit address appears in
`GET /v2/wallets/{id}` under `depositAddresses[stellarChain]`. It may take a
short time to appear. Once available, save the address and show it to the
customer.

## 3. Receive USDC and start earning

Use the Stellar deposit address to fund the customer's Portfolio Wallet with
USDC.

Subscribe to `portfolio_wallet.deposit.status_changed`. After a deposit becomes
`completed`, refresh the wallet and update the customer-facing balance.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type DepositWebhook = {
  event: "portfolio_wallet.deposit.status_changed";
  deposit: {
    id: string;
    walletId: string;
    amount: string;
    chain: string;
    status: "processing" | "completed" | "failed";
  };
};

async function handleDepositEvent(event: DepositWebhook) {
  if (event.deposit.status !== "completed") return;

  const customer = await findCustomerByGroundWalletId(
    event.deposit.walletId,
  );
  const wallet = await getCustomerWallet(event.deposit.walletId);

  await updateCustomerPortfolio(customer.id, wallet);
}
```

After the deposit completes, the USDC appears as wallet cash. Automatic
rebalancing moves eligible funds toward the configured strategy. Ground handles
bridging when a yield source is on another supported network.

## 4. Display balances and earnings

Use the customer's wallet ID to fetch their current balance and yield details:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type WalletYield = {
  walletId: string;
  earnedUsd: string;
  positions: Array<{
    yieldSourceId: string;
    name: string;
    apyBps: number | null;
    deployedValueUsd: string;
  }>;
};

async function getCustomerYield(walletId: string) {
  const response = await fetch(
    `${baseUrl}/v2/wallets/${walletId}/yield`,
    { headers: { Authorization: `Bearer ${apiToken}` } },
  );

  if (!response.ok) throw new Error(await response.text());
  return (await response.json()) as WalletYield;
}

const [currentWallet, customerYield] = await Promise.all([
  getCustomerWallet(customerWallet.id),
  getCustomerYield(customerWallet.id),
]);
```

Use these values in your customer experience:

| Value | Display |
| - | - |
| `currentWallet.balance.totalUsd` | Total portfolio balance |
| `currentWallet.balance.withdrawableUsd` | Amount currently available to withdraw |
| `customerYield.earnedUsd` | Lifetime yield earned |
| `customerYield.positions` | Yield sources, APY, and deployed value |

Dollar values are returned as decimal strings. Refresh the wallet after deposit
and withdrawal updates. You can also refresh it periodically while the customer
is viewing their portfolio.

## 5. Withdraw USDC to Stellar

First, preview the USDC withdrawal amount and Stellar destination chain. If the
preview looks correct, create the withdrawal with the customer's destination
address and a new `requestId`. Save the returned withdrawal ID in your database.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function requestStellarWithdrawal(input: {
  customerId: string;
  walletId: string;
  destinationAddress: string;
  amountUsd: string;
}) {
  const withdrawalRequest = {
    amountUsd: input.amountUsd,
    token: "usdc",
    destinationChain: stellarChain,
  };

  const previewResponse = await fetch(
    `${baseUrl}/v2/wallets/${input.walletId}/withdrawal-preview`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(withdrawalRequest),
    },
  );
  if (!previewResponse.ok) throw new Error(await previewResponse.text());
  const preview = await previewResponse.json();

  const requestId = crypto.randomUUID();
  await saveGroundRequestId(input.customerId, "withdrawal", requestId);

  const withdrawalResponse = await fetch(
    `${baseUrl}/v2/wallets/${input.walletId}/withdrawals`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        ...withdrawalRequest,
        requestId,
        destinationAddress: input.destinationAddress,
      }),
    },
  );
  if (!withdrawalResponse.ok) {
    throw new Error(await withdrawalResponse.text());
  }

  const withdrawal = await withdrawalResponse.json();
  await saveGroundWithdrawalId(input.customerId, withdrawal.id);

  return { preview, withdrawal };
}
```

The destination Stellar account must be active and configured to receive Circle
USDC on the selected network.

Subscribe to `portfolio_wallet.withdrawal.status_changed` and track the
withdrawal until it is `completed` or `failed`. A cross-chain withdrawal may
require multiple approvals. Match each approval to the saved withdrawal, verify
its details, and sign it with the organization signer. An approval does not mean
the withdrawal is complete.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.