Preview, build, sign, submit, and track a deposit request by request, in cURL or TypeScript.
A deposit takes six requests and one signature. This page shows each request
and its response, so you can build the flow in any language. For the shortest
path, start with the Quickstart.
You need the Quickstart's setup: SDP_API_URL for a beta or experimental
deployment, a sandbox API key with earn.execute, a funded test wallet in
test-wallet.json, and Node.js for signing. See
Before you begin. The cURL examples
also require jq 1.6 or later.
Work in the folder that holds test-wallet.json, with @solana/kit installed
for the signer:
List the catalog and keep an active, fundable strategy whose first deposit
mint is devnet USDC and needs no slippage floor.
Strategies explains each check.
curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/strategies?pageSize=100" \ -H "Authorization: Bearer $SDP_API_KEY" > strategies.json# The first active, fundable devnet USDC strategy that needs no deposit floor.STRATEGY_ID="$(jq -er --arg mint "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" ' [.data.strategies[] | select(.fundable and .status == "active" and .depositSlippage == null and .depositMints[0] == $mint)][0].id // error("No devnet USDC strategy is open for deposits")' strategies.json)" || exit 1export STRATEGY_IDexport AMOUNT="25" # in the deposit tokenecho "$STRATEGY_ID"
import { randomUUID } from "node:crypto";import { earnRequest } from "./earn-api";import { ownerAddress, signWithTestWallet } from "./test-wallet";type Strategy = { id: string; status: string; fundable: boolean; depositMints: string[]; depositSlippage: unknown; withdrawalSlippage: { defaultToleranceBps: number } | null;};const { strategies }: { strategies: Strategy[] } = await earnRequest( "/v1/earn/strategies?pageSize=100",);const strategy = strategies.find( (s) => s.fundable && s.status === "active" && s.depositSlippage === null && s.depositMints[0] === "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",);if (!strategy) throw new Error("No devnet USDC strategy is open for deposits");const amount = "25"; // in the deposit token
Resolve every blockingIssues entry before you build, even on a 200. A
preview does not reserve the rate. For a strategy whose depositSlippage is
not null, use sharesOut to set minSharesOut, as
Slippage floors shows.
Ignore the response's feeSponsored: it describes Treasury custody-wallet
movements, and your customer's wallet pays its own fees unless you set a
feePayer.
Keep transactionId and the base64 transaction together. The network
accepts the transaction until block height lastValidBlockHeight, about a
minute after the build, so sign and submit promptly. The customer's wallet
pays the network fee, plus rent for the accounts a first deposit creates,
unless you name a feePayer; see
Fees and sponsorship.
The customer's wallet signs the exact bytes, without changing or broadcasting
them. Here the test wallet stands in. In production, sign in your app, as the
Quickstart shows.
POST /v1/earn/external-wallet/deposits
takes the signed transaction. SDP checks it against the build, records the
deposit, then broadcasts it. The Idempotency-Key header is required.
# Save the idempotency key with the signed body before the first send; retry with both.export IDEMPOTENCY_KEY="$(node -e 'console.log(crypto.randomUUID())')"jq -nc --arg transactionId "$TRANSACTION_ID" --arg signedTransaction "$SIGNED_TRANSACTION" \ '{transactionId: $transactionId, signedTransaction: $signedTransaction}' > submit.jsoncurl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/deposits" \ -H "Authorization: Bearer $SDP_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ --data-binary @submit.json > deposit.json
// Save the idempotency key with the signed body before the first send; retry with both.const idempotencyKey = randomUUID();const { deposit } = await earnRequest( "/v1/earn/external-wallet/deposits", { transactionId: built.transactionId, signedTransaction }, idempotencyKey,);
Persist the returned positionId with the selected strategy's
withdrawalSlippage in your backend's position record. Keep null explicitly
when no floor is supported. A strategy can leave the catalog while its
positions remain withdrawable; use this saved policy when building an exit.
On some strategies a deposit's tokenAmount stays null until finalized, so
read the deposited amount there. If the response is lost or times out, send
the same body with the same idempotency key.
SDP returns the original deposit, in its current state, with
replayed: true instead of depositing twice.
Retry safely covers the other
cases.
MOVEMENT_ID="$(jq -r '.data.deposit.movementId' deposit.json)"# Every second for a minute, then every five seconds, for up to three minutes.for attempt in $(seq 84); do curl --fail-with-body -sS "${SDP_API_URL:?Set SDP_API_URL}/v1/earn/external-wallet/movements/$MOVEMENT_ID" \ -H "Authorization: Bearer $SDP_API_KEY" > movement.json jq -e '.data.movement.status | IN("finalized", "failed")' movement.json >/dev/null && break if [ "$attempt" -le 60 ]; then sleep 1; else sleep 5; fidonejq '.data.movement | {status, settlement, tokenAmount, failureReason}' movement.json
import { waitForMovement } from "./earn-api";const movement = await waitForMovement(deposit.movementId);if (movement.status === "failed") throw new Error(movement.failureReason ?? "Deposit failed");
With settlement: "atomic", you may show the deposit as done at confirmed
while you keep tracking to finalized. A provider_order deposit stays
confirmed until the provider completes it, so treat it as done only at
finalized; see
How a movement settles.