# Earn

> Coming soon, upon request — this feature is turned on for your tenant by the Aurea team. [Contact us](https://www.aureahub.com/contact)

Yield products for your users' stablecoins: a deposit from the user's own wallet, a position that grows with a variable rate, and a withdrawal back to the same wallet — through one API, whatever product serves it.

## Overview

The routes under `/v1/earn` list the products your tenant offers, quote a deposit or a withdrawal, make it from one of the user's wallets, and follow it to the end. A product is identified by its `productId` (for example `lumen` or `stablecoin-savings-usdc`) and holds one asset; its rate is variable and is always answered with the time it was read.

> Note: [Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md) Earn products are turned on per tenant.

## Products Your Tenant Has

[Coming soon, upon request](https://docs.aureahub.com/docs/feature-availability.md) Each product is turned on for your tenant on mainnet or testnet, with the smallest and largest deposit your tenant allows. Without one:

- [Products](https://docs.aureahub.com/docs/earn-products.md) answers `"products": []` and [Positions](https://docs.aureahub.com/docs/earn-positions.md) `"positions": []`;
- a quote, a deposit, a withdrawal or a broadcast answers `403` `EARN_PRODUCT_OFF` (`This product is not offered on this network.`), and a `productId` that is no product at all `404` `EARN_PRODUCT_NOT_FOUND`;
- a deposit also needs the product's disclosure, published by Aurea and accepted by the user (below): until it is published, `409` `EARN_DISCLOSURE_NOT_PUBLISHED`.

A product your tenant switched off, or one that is paused, takes no new deposit; a withdrawal of a position goes on, as long as your tenant offered the product at some point.

## Calling the Earn Routes

- Every Earn route needs **both** the user's access token (`Authorization: Bearer …`) and your tenant's request signature (`x-tenant-api-key`, `x-timestamp`, `x-signature` — see [Authentication](https://docs.aureahub.com/docs/authentication.md)). The API secret stays on your server, so call them from there, on the user's behalf.
- **Or a server token** instead of the signature: a token of one of your [server credentials](https://docs.aureahub.com/docs/guide-server-credentials.md) in `X-Aurea-Server-Token`, next to the user's `Authorization` — never with the signature headers (`400` `SERVER_AUTH_SCHEMES_CONFLICT`). The credential must be of the user's tenant (`401` `SERVER_CREDENTIAL_TENANT_MISMATCH`). A tenant on the institutional security profile must use one: its HMAC signature here answers `401` `SERVER_TOKEN_REQUIRED`.
- The token must belong to your tenant. Deposits, withdrawals, broadcasts and acceptances are for the roles that sign — `user`, `owner`, `tenantadmin`, `signer` ([Roles](https://docs.aureahub.com/docs/authentication.md)).
- Amounts are integer strings in the asset's smallest unit ([Amount Units](https://docs.aureahub.com/docs/amounts.md)); a withdrawal may say `"max"` for the whole position.
- `isTestnet` picks the network: `true` testnet, `false` mainnet.
- The writes — deposits, withdrawals, broadcasts and acceptances — allow 10 requests a minute.

## The Flow

1. **Show what is offered:** [Products](https://docs.aureahub.com/docs/earn-products.md), with each product's state and rate on every chain it is on, and your tenant's deposit limits.
2. **Before the first deposit, the disclosure:** show the user the product's disclosure ([Disclosure](https://docs.aureahub.com/docs/earn-disclosure.md) — its risks, and that its yield is variable) and record their acceptance with the `version`, `language` and `sha256` they were shown ([Accept a Disclosure](https://docs.aureahub.com/docs/earn-disclosure-accept.md)). A new version asks every user again; a withdrawal never waits for it.
3. **Quote:** [Quote](https://docs.aureahub.com/docs/earn-quote.md) makes the same checks as the write and says what it would do — the route, who signs, the wallet, the amount expected, the fee, what the wallet holds and the exact approval a deposit needs.
4. **Deposit or withdraw:** [Deposit](https://docs.aureahub.com/docs/earn-deposit.md) or [Withdraw](https://docs.aureahub.com/docs/earn-withdraw.md) answers the operation.
5. **Sign, when the wallet is the user's own:** send the signatures with [Send Signatures](https://docs.aureahub.com/docs/earn-broadcast.md).
6. **Follow it:** [Get an Operation](https://docs.aureahub.com/docs/earn-operation-get.md) and [Positions](https://docs.aureahub.com/docs/earn-positions.md).

## Who Signs

The wallet decides. Name it with `walletId` — one of the user's EVM wallets on the network — or leave it out, and Aurea takes the first wallet somebody can sign for on the product's chain, then on any EVM chain, the primary first. A watch-only import is never used (`400` `EARN_WALLET_NOT_SIGNABLE`).

| Wallet | What happens |
| --- | --- |
| **Custodial (the platform holds the key for your tenant)** | `signer: "server"`. Aurea signs and sends every step at once and answers the operation `completed`, `failed`, or `broadcast` when a receipt is late. |
| **Non-custodial (`client_side`) or MPC (`mpc_tss`)** | `signer: "client"`. The operation is answered `submitted` with `signing.steps`. Sign each step's `txHashToSign` with the wallet — the device key, or the MPC signing ceremony — and send the signatures, in the order of the steps, to [Send Signatures](https://docs.aureahub.com/docs/earn-broadcast.md) before `signing.expiresAt`. Aurea checks each against the wallet and sends the transactions exactly as it built them: you never send a transaction. |

Each step says what it is — `purpose` (`approve`, `deposit` or `withdraw`), `amount` and `counterparty` — so the user can be shown what they sign. A deposit's approval is of exactly the amount, never more, and only when the allowance does not already cover it.

## Routes and Chains

A product may be reached in more than one way. Without `route`, Aurea takes the first open to the wallet, in this order; with it, that route or `409` `EARN_ROUTE_NOT_OFFERED` (with the open `routes`).

- `direct` — into the product on its own chain.
- `bridge` — from another chain, through a cross-chain router: the operation is `bridging` while the message travels (`bridge.messageId`) and `completed` once the product took it. The fee of the message is paid with the transaction in the chain's native token (`nativeFee` in the quote). The operation then adds `bridge.destination` once Aurea has found the message's delivery, shortly after it completed: `chainId` (the product's chain), `txHash` (the transaction that delivered the message there) and `blockNumber` (decimal text).
- `secondary_market` — the product's share token bought or sold on the market, at once.

`chainId` picks the chain; without it, the one the wallet is on. A chain the product is not on answers `400` `EARN_CHAIN_NOT_OFFERED` (with the product's `chains`).

## Withdrawals and Redemptions

A withdrawal pays to the wallet the position is held by. Some products pay a withdrawal later: the operation is `completed` once the request is made, and its `redemption` follows the request until it is paid:

```text
requested → queued → paid
              ↘ failed | cancelled (the shares returned)
```

`queued` means waiting for the product's liquidity — usually under two days, at most about thirty. `assetsPaid` and `paidTxHash` are filled in when it is paid.

## Operation States

```text
submitted → broadcast → bridging (bridge route only) → completed
                                                      ↘ failed
submitted → expired   (the client's signatures never came)
```

States only move forward. Reading an operation brings it up to date first: a `broadcast` one is read from its chain and completed or failed when its receipts say so; one whose signatures never came in time is `expired`. A `failed` operation carries `failureCode`.

## Readings and Limits

- A product's state and rate are read at one block, and `observedAt` is that block's time. `fresh` says whether the reading is recent enough to decide a deposit or a withdrawal on.
- Before a deposit, a withdrawal or a broadcast, Aurea reads the product again: a reading older than the Hub's limit answers `503` `EARN_READING_STALE`, an unreadable one `503` `EARN_READING_FAILED`, a product not taking the operation on chain `409` `EARN_PRODUCT_NOT_ACCEPTING`. A quote answers the reading's age and state instead of refusing.
- The state on chain: `active`; `frozen` (withdrawals only); `paused` or `inactive` (nothing).
- Your tenant's limits apply to each deposit (`400` `EARN_AMOUNT_OUT_OF_LIMITS`); the wallet must hold the amount (`400` `EARN_INSUFFICIENT_BALANCE`), and the position the amount withdrawn (`400` `EARN_INSUFFICIENT_POSITION`).

## Retries

Deposits, withdrawals and broadcasts take an optional `Idempotency-Key` header, up to 255 letters, digits, `-` or `_`, and their answers always carry `idempotency-replayed`:

- the same key with the same request answers the first answer again, with `idempotency-replayed: true`, and nothing is signed or sent twice;
- the same key with another request answers `409` `IDEMPOTENCY_KEY_REUSED`; while the first is still running, `409` `IDEMPOTENCY_IN_PROGRESS`; a key that is not one, `400` `IDEMPOTENCY_KEY_INVALID`;
- the checks that need no chain — the product switched on, the disclosure accepted — are made before a key is replayed. A deposit refused for its disclosure frees its key: the same key deposits once the user has accepted.

Once a transaction is out, a write does not fail: it answers the operation in the state it reached, so a retry is answered rather than sent again.

## Errors

Earn's refusals carry their code in `details.code`, with what it is about beside it (`productId`, `operationId`…):

```json
{
  "statusCode": 403,
  "error": "ForbiddenError",
  "message": "This product is not offered on this network.",
  "details": { "code": "EARN_PRODUCT_OFF", "productId": "lumen" }
}
```

| Status | details.code | When |
| --- | --- | --- |
| 400 | EARN_AMOUNT_OUT_OF_LIMITS | Below or above your tenant's deposit limits, or below what the bridge's fee leaves (`limit`, `minimum`). |
| 400 | EARN_INSUFFICIENT_BALANCE | The wallet does not hold the amount. |
| 400 | EARN_INSUFFICIENT_POSITION | The position does not hold the amount. |
| 400 | EARN_NO_WALLET | The user has no wallet that can be used. |
| 400 | EARN_WALLET_NOT_SIGNABLE | A watch-only address. |
| 400 | EARN_CHAIN_NOT_OFFERED | The product is not on that chain. |
| 400 | EARN_SIGNATURES_MISMATCH | Not one signature per step. |
| 400 | EARN_BROADCAST_REFUSED | The node refused the first step (`reason`: `insufficient_gas`, `nonce_conflict`, `reverted` or `rejected`); nothing was sent, and the same signatures may be sent again. |
| 403 | EARN_PRODUCT_OFF | Your tenant does not offer the product on this network (for a deposit: no longer). |
| 403 | EARN_TENANT_REQUIRED | A token without a tenant. |
| 404 | EARN_PRODUCT_NOT_FOUND | No product with this identifier. |
| 404 | EARN_WALLET_NOT_FOUND | `walletId` is not one of the user's wallets on the network. |
| 404 | EARN_OPERATION_NOT_FOUND | Not one of the user's operations. |
| 409 | EARN_PRODUCT_PAUSED | The product takes no new deposit. |
| 409 | EARN_PRODUCT_NOT_ACCEPTING | The product does not take the operation on chain now (`reason`). |
| 409 | EARN_ROUTE_NOT_OFFERED | The route asked for is not open to the wallet (`routes`). |
| 409 | EARN_OPERATION_NOT_AWAITING_SIGNATURES | The operation is in another state. |
| 409 | EARN_DISCLOSURE_NOT_PUBLISHED | The product's disclosure is not published: no deposit yet. |
| 409 | EARN_DISCLOSURE_NOT_ACCEPTED | The user has not accepted the current version (`version`). |
| 409 | EARN_DISCLOSURE_VERSION_MISMATCH | The acceptance is not of the current text; nothing is recorded. |
| 410 | EARN_SIGNING_EXPIRED | Past `signing.expiresAt`. |
| 422 | EARN_SIGNATURE_INVALID | A signature not made by the operation's wallet, or a `v` that is none of 0, 1, 27 and 28 (27 and 28 in decimal or hexadecimal). |
| 500 | EARN_SIGNING_FAILED | Aurea could not sign for a custodial wallet. |
| 503 | EARN_READING_STALE | The product's reading is too old to decide on. |
| 503 | EARN_READING_FAILED | The product could not be read. |
| 503 | EARN_PRODUCT_UNAVAILABLE | The product cannot be reached now. |
| 503 | EARN_ROUTE_UNAVAILABLE | The route cannot be used now (for the market: `reason`). |

[Contact us for details](https://www.aureahub.com/contact).

---

Web version: https://docs.aureahub.com/#guide-earn
