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

# Earn

> The Earn API exposes Auto-Earn: enable or disable yield generation across eligible assets, read the user's allocations and historical rewards, and discover which assets are auto-earn eligible by user or by country.

## Overview

Earn is exposed as a single product called **Auto-Earn**. You don't allocate balances per strategy — you flip a per-yield-source switch for the user and Payward picks strategies and routes eligible balances. Payward absorbs any on-chain bonding or unbonding periods, so allocated assets stay liquid and remain at the user's disposal at all times. The supported yield sources are `staking` and `base_rewards`; more may be added without breaking the response shape.

The integration shape is six endpoints:

| Goal                                           | Endpoint                                          | Method |
| ---------------------------------------------- | ------------------------------------------------- | ------ |
| Discover Auto-Earn eligibility by country      | `/v1/earn/auto/assets`                            | GET    |
| Discover Auto-Earn eligibility for an account  | `/v1/accounts/{account_id}/earn/auto/assets`      | GET    |
| Set Auto-Earn preferences (per yield source)   | `/v1/accounts/{account_id}/earn/auto`             | PATCH  |
| Read current preferences (incl. pending state) | `/v1/accounts/{account_id}/earn/auto`             | GET    |
| List the user's current per-asset allocations  | `/v1/accounts/{account_id}/earn/auto/allocations` | GET    |
| List historical rewards & next payout estimate | `/v1/accounts/{account_id}/earn/auto/rewards`     | GET    |

<Note>
  If you have used Kraken's [Spot REST Earn](https://docs.kraken.com/api/docs/guides/spot-rest-earn/) endpoints
  (`Strategies`, `Allocations`, `Allocate`, `Deallocate`), the PWS Earn API is a higher-level abstraction of the same
  product. PWS picks strategies, routes eligible balances, and hides any on-chain bonding or unbonding so allocated
  assets remain liquid for the user. You see the result through the six endpoints above and through the Portfolio API,
  which surfaces each settled reward as an `earn_reward` transaction.
</Note>

## Prerequisites

* Payward Services API credentials (see the [Authentication guide](/tabs/developer-documentation/get-started/authentication)).
* A verified user with at least one account and sufficient balance in the source asset.
* Examples target `https://api.services.payward.com` and read credentials from the `PWS_API_KEY` and `PWS_API_SECRET` environment variables.

## Workflow

<Steps>
  <Step title="Discover Auto-Earn assets (optional, pre-account)">
    Show what's available in a country before you have an account. User-agnostic.

    `GET /v1/earn/auto/assets?country=US`
  </Step>

  <Step title="Discover Auto-Earn assets for an account">
    Show APY and per-user allocation cap for the account's eligible assets.

    `GET /v1/accounts/{account_id}/earn/auto/assets`
  </Step>

  <Step title="Set Auto-Earn preferences">
    Enable or disable a yield source. Async.

    `PATCH /v1/accounts/{account_id}/earn/auto`
  </Step>

  <Step title="Poll until preferences settle">
    Read current preferences and watch `pending_enabled` / `pending_disabled` resolve.

    `GET /v1/accounts/{account_id}/earn/auto`
  </Step>

  <Step title="Read allocations and rewards">
    Show how much is currently earning, and historical / upcoming rewards.

    * `GET /v1/accounts/{account_id}/earn/auto/allocations`
    * `GET /v1/accounts/{account_id}/earn/auto/rewards`
  </Step>
</Steps>

## Auto-Earn preference lifecycle

Each yield source has one of four states. **Only the yield sources the user is eligible for appear** in `GET /v1/accounts/{account_id}/earn/auto`; missing fields mean the user can't enable that source today and the corresponding field on `PATCH` is a no-op. There is no separate eligibility endpoint — presence in this response is the eligibility check.

| State              | Meaning                                                | What to show                                          |
| ------------------ | ------------------------------------------------------ | ----------------------------------------------------- |
| `enabled`          | Yield source is active. Eligible balances are earning. | "On".                                                 |
| `disabled`         | Yield source is off. No new allocations.               | "Off".                                                |
| `pending_enabled`  | `PATCH` accepted; provisioning in progress.            | "Enabling…" with a spinner. Don't allow re-toggling.  |
| `pending_disabled` | `PATCH` accepted; unallocation in progress.            | "Disabling…" with a spinner. Don't allow re-toggling. |

`PATCH /v1/accounts/{account_id}/earn/auto` returns immediately with an empty body. Poll `GET /v1/accounts/{account_id}/earn/auto` until the pending state resolves.

<Warning>
  Only one Auto-Earn preference change can be in progress per account at a time. Submitting a `PATCH` while any yield
  source is in `pending_enabled` or `pending_disabled` returns `409 Conflict`. Wait for the pending state to resolve in
  `GET /v1/accounts/{account_id}/earn/auto` before issuing the next change.
</Warning>

## Asset, amount, and quote-currency conventions

Allocation and reward entries describe an **asset** at the top level using three fields:

| Field    | Description                                            |
| -------- | ------------------------------------------------------ |
| `symbol` | Ticker symbol (e.g. `ETH`, `EUR`).                     |
| `type`   | `fiat`, `crypto`, `stablecoin`, `xstock`, or `equity`. |
| `name`   | Human-readable name (e.g. `"Ethereum"`).               |

All **monetary fields inside an entry** are plain decimal strings:

| Suffix      | Denominated in                                                                                           |
| ----------- | -------------------------------------------------------------------------------------------------------- |
| (none)      | The entry's own asset. `allocated: "100.1234"` means 100.1234 ETH on an `ETH` entry.                     |
| `_in_quote` | The request's quote currency. `allocated_in_quote: "12345.12"` is 12,345.12 EUR when `quote_symbol=EUR`. |

The allocations and rewards endpoints accept `quote_symbol` and `quote_type` to choose the quote asset, and **echo both at the top level of the response** so you can format `*_in_quote` values without re-stating the choice. Today only `quote_type=fiat` is supported; `quote_symbol` defaults to `USD`. Pass the same values across Earn and the Portfolio API so totals reconcile.

## Authentication setup

Authenticated endpoints require an HMAC-SHA512 request signature in the `API-Sign` header and a monotonically increasing nonce in the `API-Nonce` header. The helper below derives the signature from the URL path, request body, and nonce. See the [Authentication guide](/tabs/developer-documentation/get-started/authentication) for the full algorithm.

<CodeGroup>
  ```python Python theme={null}
  import os
  import json
  import time
  import uuid
  import hashlib
  import hmac
  import base64
  import urllib.parse
  import requests

  API_KEY = os.environ["PWS_API_KEY"]
  API_SECRET = os.environ["PWS_API_SECRET"]
  BASE_URL = "https://api.services.payward.com"


  def get_payward_signature(urlpath, data, secret, nonce, params=None):
      encoded = (
          str(nonce).encode("utf-8")
          if data is None
          else (str(nonce) + json.dumps(data)).encode("utf-8")
      )

      sign_path = urlpath
      if params:
          sign_path += "?" + urllib.parse.urlencode(params)

      message = sign_path.encode() + hashlib.sha256(encoded).digest()
      mac = hmac.new(base64.b64decode(secret), message, hashlib.sha512)
      return base64.b64encode(mac.digest()).decode()


  def pws_headers(signature, nonce, idempotent=False):
      headers = {
          "API-Key": API_KEY,
          "API-Sign": signature,
          "API-Nonce": str(nonce),
          "Content-Type": "application/json",
      }
      if idempotent:
          headers["Idempotency-Key"] = str(uuid.uuid4())
      return headers
  ```

  ```javascript Javascript theme={null}
  import crypto from 'crypto';

  const API_KEY = process.env.PWS_API_KEY;
  const API_SECRET = process.env.PWS_API_SECRET;
  const BASE_URL = 'https://api.services.payward.com';

  function getPaywardSignature(urlpath, data, secret, nonce, params = null) {
    const encoded = data === null ? String(nonce) : String(nonce) + JSON.stringify(data);

    let signPath = urlpath;
    if (params) {
      signPath += '?' + new URLSearchParams(params).toString();
    }

    const sha256Hash = crypto.createHash('sha256').update(encoded).digest();
    const message = Buffer.concat([Buffer.from(signPath), sha256Hash]);

    const secretBuffer = Buffer.from(secret, 'base64');
    const hmac = crypto.createHmac('sha512', secretBuffer);
    hmac.update(message);
    return hmac.digest('base64');
  }

  function pwsHeaders(signature, nonce, { idempotent = false } = {}) {
    const headers = {
      'API-Key': API_KEY,
      'API-Sign': signature,
      'API-Nonce': String(nonce),
      'Content-Type': 'application/json',
    };
    if (idempotent) {
      headers['Idempotency-Key'] = crypto.randomUUID();
    }
    return headers;
  }
  ```
</CodeGroup>

<Note>
  Every PWS write endpoint (`POST` / `PUT` / `DELETE`) accepts an `Idempotency-Key` header containing a UUIDv4. Generate
  a fresh key per logical attempt — replays of the same key return the original response body and the
  `Idempotent-Replayed: true` response header, which keeps retries safe under timeouts and connection drops.
</Note>

The Earn endpoints either accept query parameters or a JSON body. Include the query string in the path you sign — the helper supports this through its `params` argument.

## Step 1: discover Auto-Earn assets by country

`GET /v1/earn/auto/assets` is **user-agnostic**. Use it to surface the value proposition before sign-up — for example on a landing page filtered by the visitor's country. Pagination is via `page_token` / `page_size`.

<CodeGroup>
  ```python Python theme={null}
  def list_auto_earn_assets_by_country(country, page_size=20, page_token=None):
      endpoint = "/v1/earn/auto/assets"
      nonce = time.time_ns()
      params = {"country": country, "page_size": str(page_size)}
      if page_token:
          params["page_token"] = page_token

      signature = get_payward_signature(endpoint, None, API_SECRET, nonce, params=params)
      response = requests.get(
          BASE_URL + endpoint,
          headers=pws_headers(signature, nonce),
          params=params,
      )
      return response.json()


  catalog = list_auto_earn_assets_by_country("US")
  for asset in catalog["data"]:
      print(f"{asset['symbol']} ({asset['name']}): {asset['apy']}% APY, cap {asset['user_cap']} {asset['symbol']}")
  ```

  ```javascript Javascript theme={null}
  async function listAutoEarnAssetsByCountry(country, pageSize = 20, pageToken = null) {
    const endpoint = '/v1/earn/auto/assets';
    const nonce = process.hrtime.bigint().toString();
    const params = new URLSearchParams({ country, page_size: String(pageSize) });
    if (pageToken) params.set('page_token', pageToken);

    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce, params);
    const response = await fetch(`${BASE_URL}${endpoint}?${params}`, {
      method: 'GET',
      headers: pwsHeaders(signature, nonce),
    });

    return response.json();
  }

  const catalog = await listAutoEarnAssetsByCountry('US');
  for (const asset of catalog.data) {
    console.log(`${asset.symbol} (${asset.name}): ${asset.apy}% APY, cap ${asset.user_cap} ${asset.symbol}`);
  }
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "data": [
    {
      "symbol": "ETH",
      "type": "crypto",
      "name": "Ethereum",
      "yield_source": "staking",
      "apy": "3.41",
      "user_cap": "1000"
    }
  ],
  "next_page_token": null
}
```

| Field             | How to use it                                                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `yield_source`    | Yield source that backs this asset (`staking` or `base_rewards`).                                                                           |
| `apy`             | Estimated annual percentage yield as a decimal string (e.g. `"3.41"` for 3.41%).                                                            |
| `user_cap`        | Hard maximum a single user is allowed to auto-earn for this asset, in the entry's asset. Show as a "max" hint. Absent when there is no cap. |
| `next_page_token` | Opaque cursor; pass back as `page_token` to fetch the next page.                                                                            |

<Tip>
  APY is an estimate, not a guarantee. Display the value with a `%` suffix and label it "estimated", since realized
  yield depends on network conditions and fees.
</Tip>

## Step 2: discover Auto-Earn assets for an account

`GET /v1/accounts/{account_id}/earn/auto/assets` returns the same shape as Step 1 but scoped to a specific account. Use it after sign-up to confirm exactly what that account can auto-earn.

<CodeGroup>
  ```python Python theme={null}
  def list_account_auto_earn_assets(account_id, page_size=20, page_token=None):
      endpoint = f"/v1/accounts/{account_id}/earn/auto/assets"
      nonce = time.time_ns()
      params = {"page_size": str(page_size)}
      if page_token:
          params["page_token"] = page_token

      signature = get_payward_signature(endpoint, None, API_SECRET, nonce, params=params)
      response = requests.get(
          BASE_URL + endpoint,
          headers=pws_headers(signature, nonce),
          params=params,
      )
      return response.json()


  account_id = "WFFGB6Z8AK5WF9JL"
  account_assets = list_account_auto_earn_assets(account_id)
  for asset in account_assets["data"]:
      print(f"{asset['symbol']}: {asset['apy']}% APY (cap {asset['user_cap']} {asset['symbol']})")
  ```

  ```javascript Javascript theme={null}
  async function listAccountAutoEarnAssets(accountId, pageSize = 20, pageToken = null) {
    const endpoint = `/v1/accounts/${accountId}/earn/auto/assets`;
    const nonce = process.hrtime.bigint().toString();
    const params = new URLSearchParams({ page_size: String(pageSize) });
    if (pageToken) params.set('page_token', pageToken);

    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce, params);
    const response = await fetch(`${BASE_URL}${endpoint}?${params}`, {
      method: 'GET',
      headers: pwsHeaders(signature, nonce),
    });

    return response.json();
  }

  const accountId = 'WFFGB6Z8AK5WF9JL';
  const accountAssets = await listAccountAutoEarnAssets(accountId);
  for (const asset of accountAssets.data) {
    console.log(`${asset.symbol}: ${asset.apy}% APY (cap ${asset.user_cap} ${asset.symbol})`);
  }
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "data": [
    {
      "symbol": "ETH",
      "type": "crypto",
      "name": "Ethereum",
      "yield_source": "staking",
      "apy": "3.41",
      "user_cap": "1000"
    }
  ],
  "next_page_token": null
}
```

## Step 3: set Auto-Earn preferences

Send a `PATCH` to `/v1/accounts/{account_id}/earn/auto` with the desired state of each yield source you want to change. **All body fields are optional** — yield sources you omit keep their current preference, so you can flip a single source without touching the others.

Supported yield sources: `staking`, `base_rewards`. Allowed values: `"enabled"`, `"disabled"`.

<CodeGroup>
  ```python Python theme={null}
  def set_auto_earn_preferences(account_id, preferences):
      endpoint = f"/v1/accounts/{account_id}/earn/auto"
      nonce = time.time_ns()

      signature = get_payward_signature(endpoint, preferences, API_SECRET, nonce)
      response = requests.patch(
          BASE_URL + endpoint,
          headers=pws_headers(signature, nonce, idempotent=True),
          json=preferences,
      )
      return response.status_code, response.json()


  status, body = set_auto_earn_preferences(account_id, {"staking": "enabled"})
  print(f"Toggle accepted: HTTP {status}")
  ```

  ```javascript Javascript theme={null}
  async function setAutoEarnPreferences(accountId, preferences) {
    const endpoint = `/v1/accounts/${accountId}/earn/auto`;
    const nonce = process.hrtime.bigint().toString();

    const signature = getPaywardSignature(endpoint, preferences, API_SECRET, nonce);
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'PATCH',
      headers: pwsHeaders(signature, nonce, { idempotent: true }),
      body: JSON.stringify(preferences),
    });

    return { status: response.status, body: await response.json() };
  }

  const { status } = await setAutoEarnPreferences(accountId, { staking: 'enabled' });
  console.log(`Toggle accepted: HTTP ${status}`);
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "data": {}
}
```

<Note>
  The `200 OK` confirms the preference change was accepted, not that every eligible balance is already earning.
  Provisioning runs asynchronously; observe `pending_enabled` / `pending_disabled` on `GET /v1/accounts/{account_id}
      /earn/auto` to know when the change has settled. A second `PATCH` issued before the pending state resolves returns
  `409 Conflict` — see the [preference lifecycle](#auto-earn-preference-lifecycle) above.
</Note>

## Step 4: poll until preferences settle

`GET /v1/accounts/{account_id}/earn/auto` returns the current preference for every yield source the user is eligible for. Eligibility is country-aware: if a user moves to a region where staking isn't supported, the `staking` field disappears from this response and the corresponding field on `PATCH` becomes a no-op.

<CodeGroup>
  ```python Python theme={null}
  def get_auto_earn_preferences(account_id):
      endpoint = f"/v1/accounts/{account_id}/earn/auto"
      nonce = time.time_ns()

      signature = get_payward_signature(endpoint, None, API_SECRET, nonce)
      response = requests.get(BASE_URL + endpoint, headers=pws_headers(signature, nonce))
      return response.json()


  def wait_for_settled(account_id, source, max_attempts=60):
      pending = {"pending_enabled", "pending_disabled"}
      for _ in range(max_attempts):
          prefs = get_auto_earn_preferences(account_id)
          state = prefs.get(source)
          print(f"{source}: {state}")
          if state is None or state not in pending:
              return prefs
          time.sleep(2)
      raise RuntimeError(f"{source} did not settle in time")


  prefs = wait_for_settled(account_id, "staking")
  ```

  ```javascript Javascript theme={null}
  async function getAutoEarnPreferences(accountId) {
    const endpoint = `/v1/accounts/${accountId}/earn/auto`;
    const nonce = process.hrtime.bigint().toString();

    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce);
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'GET',
      headers: pwsHeaders(signature, nonce),
    });

    return response.json();
  }

  async function waitForSettled(accountId, source, maxAttempts = 60) {
    const pending = new Set(['pending_enabled', 'pending_disabled']);
    for (let i = 0; i < maxAttempts; i++) {
      const prefs = await getAutoEarnPreferences(accountId);
      const state = prefs[source];
      console.log(`${source}: ${state}`);
      if (state === undefined || !pending.has(state)) return prefs;
      await new Promise(resolve => setTimeout(resolve, 2000));
    }
    throw new Error(`${source} did not settle in time`);
  }

  const prefs = await waitForSettled(accountId, 'staking');
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "data": {
    "staking": "enabled"
  }
}
```

## Step 5a: list current allocations

`GET /v1/accounts/{account_id}/earn/auto/allocations` returns the user's current per-asset allocation, plus the all-asset total in the requested quote currency. The endpoint is cursor-paginated; `next_page_token` is absent on the last page. All amounts inside entries are plain decimal strings; the response echoes `quote_symbol` / `quote_type` at the top level so you know how to format `*_in_quote` values.

<CodeGroup>
  ```python Python theme={null}
  def list_auto_earn_allocations(account_id, quote_symbol="USD", page_token=None, page_size=20):
      endpoint = f"/v1/accounts/{account_id}/earn/auto/allocations"
      nonce = time.time_ns()
      params = {
          "quote_symbol": quote_symbol,
          "quote_type": "fiat",
          "page_size": str(page_size),
      }
      if page_token:
          params["page_token"] = page_token

      signature = get_payward_signature(endpoint, None, API_SECRET, nonce, params=params)
      response = requests.get(
          BASE_URL + endpoint,
          headers=pws_headers(signature, nonce),
          params=params,
      )
      return response.json()


  allocations = list_auto_earn_allocations(account_id, quote_symbol="EUR")
  quote = allocations["quote_symbol"]
  print(f"Total allocated: {allocations['total_allocated_in_quote']} {quote}")
  for entry in allocations["data"]:
      print(
          f"  {entry['allocated']} {entry['symbol']} "
          f"(~{entry['allocated_in_quote']} {quote})"
      )
  ```

  ```javascript Javascript theme={null}
  async function listAutoEarnAllocations(accountId, quoteSymbol = 'USD', pageToken = null, pageSize = 20) {
    const endpoint = `/v1/accounts/${accountId}/earn/auto/allocations`;
    const nonce = process.hrtime.bigint().toString();
    const params = new URLSearchParams({
      quote_symbol: quoteSymbol,
      quote_type: 'fiat',
      page_size: String(pageSize),
    });
    if (pageToken) params.set('page_token', pageToken);

    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce, params);
    const response = await fetch(`${BASE_URL}${endpoint}?${params}`, {
      method: 'GET',
      headers: pwsHeaders(signature, nonce),
    });

    return response.json();
  }

  const allocations = await listAutoEarnAllocations(accountId, 'EUR');
  const quote = allocations.quote_symbol;
  console.log(`Total allocated: ${allocations.total_allocated_in_quote} ${quote}`);
  for (const entry of allocations.data) {
    console.log(`  ${entry.allocated} ${entry.symbol} (~${entry.allocated_in_quote} ${quote})`);
  }
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "quote_symbol": "EUR",
  "quote_type": "fiat",
  "total_allocated_in_quote": "12345.12",
  "data": [
    {
      "symbol": "ETH",
      "type": "crypto",
      "name": "Ethereum",
      "allocated": "100.1234",
      "allocated_in_quote": "12345.12"
    }
  ],
  "next_page_token": null
}
```

| Field                         | How to use it                                                                          |
| ----------------------------- | -------------------------------------------------------------------------------------- |
| `quote_symbol` / `quote_type` | Echo of the requested quote asset. Use to format `*_in_quote` values.                  |
| `total_allocated_in_quote`    | Cross-asset allocation total in the quote currency. Use it for a dashboard hero value. |
| `data[].allocated`            | Per-asset amount currently earning, in the entry's asset.                              |
| `data[].allocated_in_quote`   | Same per-asset amount in the quote currency.                                           |
| `next_page_token`             | Opaque cursor; pass back as `page_token` for the next page.                            |

<Note>
  Auto-Earn allocations are liquid. The `allocated` amount is the user's current earning balance and is always at the
  user's disposal — PWS absorbs any on-chain bonding or unbonding so there are no "in-transit" amounts for you to track
  on the Earn side.
</Note>

## Step 5b: list rewards

`GET /v1/accounts/{account_id}/earn/auto/rewards` returns historical rewards per asset, the all-time total in the quote currency, and the next payout estimate. Same `quote_symbol` / `quote_type` / `page_token` / `page_size` semantics as allocations, and the same envelope (`quote_symbol` / `quote_type` echoed at the top level).

`estimated_next_reward`, `estimated_next_reward_in_quote`, and the top-level `next_reward_date` are absent when no reward is pending. `other_assets` is absent on an entry when every reward for that asset was paid in the entry's own asset.

<CodeGroup>
  ```python Python theme={null}
  def list_auto_earn_rewards(account_id, quote_symbol="USD", page_token=None, page_size=20):
      endpoint = f"/v1/accounts/{account_id}/earn/auto/rewards"
      nonce = time.time_ns()
      params = {
          "quote_symbol": quote_symbol,
          "quote_type": "fiat",
          "page_size": str(page_size),
      }
      if page_token:
          params["page_token"] = page_token

      signature = get_payward_signature(endpoint, None, API_SECRET, nonce, params=params)
      response = requests.get(
          BASE_URL + endpoint,
          headers=pws_headers(signature, nonce),
          params=params,
      )
      return response.json()


  rewards = list_auto_earn_rewards(account_id, quote_symbol="EUR")
  quote = rewards["quote_symbol"]
  print(f"Total rewarded: {rewards['total_rewarded_in_quote']} {quote}")
  if rewards.get("next_reward_date"):
      print(f"Next payout: {rewards['next_reward_date']}")

  for entry in rewards["data"]:
      line = f"  {entry['symbol']}: rewarded {entry['rewarded']} {entry['symbol']}"
      if "estimated_next_reward" in entry:
          line += f", next ~{entry['estimated_next_reward']} {entry['symbol']}"
      print(line)
      for side in entry.get("other_assets", []):
          print(
              f"    + {side['rewarded']} {side['symbol']} "
              f"(~{side['rewarded_in_quote']} {quote})"
          )
  ```

  ```javascript Javascript theme={null}
  async function listAutoEarnRewards(accountId, quoteSymbol = 'USD', pageToken = null, pageSize = 20) {
    const endpoint = `/v1/accounts/${accountId}/earn/auto/rewards`;
    const nonce = process.hrtime.bigint().toString();
    const params = new URLSearchParams({
      quote_symbol: quoteSymbol,
      quote_type: 'fiat',
      page_size: String(pageSize),
    });
    if (pageToken) params.set('page_token', pageToken);

    const signature = getPaywardSignature(endpoint, null, API_SECRET, nonce, params);
    const response = await fetch(`${BASE_URL}${endpoint}?${params}`, {
      method: 'GET',
      headers: pwsHeaders(signature, nonce),
    });

    return response.json();
  }

  const rewards = await listAutoEarnRewards(accountId, 'EUR');
  const quote = rewards.quote_symbol;
  console.log(`Total rewarded: ${rewards.total_rewarded_in_quote} ${quote}`);
  if (rewards.next_reward_date) {
    console.log(`Next payout: ${rewards.next_reward_date}`);
  }

  for (const entry of rewards.data) {
    let line = `  ${entry.symbol}: rewarded ${entry.rewarded} ${entry.symbol}`;
    if (entry.estimated_next_reward !== undefined) {
      line += `, next ~${entry.estimated_next_reward} ${entry.symbol}`;
    }
    console.log(line);
    for (const side of entry.other_assets ?? []) {
      console.log(`    + ${side.rewarded} ${side.symbol} (~${side.rewarded_in_quote} ${quote})`);
    }
  }
  ```
</CodeGroup>

### Response example

```json theme={null}
{
  "quote_symbol": "EUR",
  "quote_type": "fiat",
  "total_rewarded_in_quote": "325262.124",
  "next_reward_date": "2026-05-07T16:57:51Z",
  "data": [
    {
      "symbol": "ETH",
      "type": "crypto",
      "name": "Ethereum",
      "rewarded": "13.5",
      "rewarded_in_quote": "2451.12",
      "estimated_next_reward": "0.56",
      "estimated_next_reward_in_quote": "101.23",
      "other_assets": [
        {
          "symbol": "EIGEN",
          "type": "crypto",
          "name": "EigenLayer",
          "rewarded": "5.0",
          "rewarded_in_quote": "123.45",
          "estimated_next_reward": "0.1",
          "estimated_next_reward_in_quote": "2.34"
        }
      ]
    }
  ],
  "next_page_token": null
}
```

| Field                                        | How to use it                                                                                                                                                                           |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quote_symbol` / `quote_type`                | Echo of the requested quote asset. Use to format `*_in_quote` values.                                                                                                                   |
| `total_rewarded_in_quote`                    | All-time, cross-asset rewards in the quote currency. Headline figure for the rewards card.                                                                                              |
| `next_reward_date`                           | Timestamp of the next payout across all assets. Absent when nothing is pending.                                                                                                         |
| `data[].rewarded` / `_in_quote`              | Per-asset all-time rewards, in the entry's asset and in the quote currency.                                                                                                             |
| `data[].estimated_next_reward` / `_in_quote` | Best-effort estimate of the upcoming reward for that asset. Absent when no payout is pending.                                                                                           |
| `data[].other_assets`                        | Rewards paid in a different asset than the entry's own (e.g. restaking-style side rewards). Same shape minus a nested `other_assets`. Absent when all rewards are in the entry's asset. |
| `next_page_token`                            | Opaque cursor; pass back as `page_token` for the next page.                                                                                                                             |

<Note>
  Reward amounts can be very small for low-APY assets — a value like `"0.00000001"` is common. Decide up front how to
  render near-zero values (for example, "\<0.00000001 ETH" or "\~0").
</Note>

## Reconciling rewards in Portfolio

When a payout settles, it appears in the Portfolio activity feed as an `earn_reward` transaction:

```bash theme={null}
curl -X GET "https://api.services.payward.com/v1/accounts/WFFGB6Z8AK5WF9JL/portfolio/transactions?types=earn_reward&start_timestamp=2026-04-01T00:00:00Z&end_timestamp=2026-04-30T23:59:59Z&quote_symbol=USD" \
  -H "API-Key: $PWS_API_KEY" \
  -H "API-Nonce: $PWS_API_NONCE" \
  -H "API-Sign: $PWS_API_SIGN"
```

Use the [Portfolio guide](/tabs/developer-documentation/accounts/portfolio) for the activity feed and pagination, and the Reports endpoints for downloadable settlement statements.

## Error handling

All error responses share a standard envelope:

```json theme={null}
{
  "error": {
    "type": "earn_error",
    "status": 400,
    "code": "bad_request",
    "instance": "req-abc123",
    "causes": [
      {
        "field": "country",
        "message": "must match pattern \"^[A-Z]{2}$\""
      }
    ]
  }
}
```

| Field      | Description                                                                                                                   |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `type`     | Error class (e.g. `earn_error`).                                                                                              |
| `status`   | HTTP status code.                                                                                                             |
| `code`     | Machine-readable error code in snake\_case (e.g. `bad_request`, `not_eligible`).                                              |
| `instance` | Request-Id for this occurrence — include it in support tickets.                                                               |
| `doc_url`  | Optional link to documentation for this error.                                                                                |
| `causes`   | Optional array of per-field validation failures. Each entry has a `field` (dot-notation path) and a human-readable `message`. |

| HTTP status             | Cause                                                                                                                              | Remediation                                                                                                                    |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `400 Bad Request`       | Invalid `country` (not ISO 3166-1 alpha-2), unsupported `quote_type`, bad enum value in the `PATCH` body, or bad pagination tokens | Validate query parameters and body before sending. Inspect `causes[]` for per-field details.                                   |
| `401 Unauthorized`      | Missing or invalid signature, key, or nonce                                                                                        | Verify `API-Key`, `API-Sign`, and `API-Nonce`. The nonce must increase.                                                        |
| `403 Forbidden`         | Account is not authorized to use Earn                                                                                              | Confirm the account's product entitlements.                                                                                    |
| `404 Not Found`         | Unknown `account_id`                                                                                                               | Verify the `account_id` belongs to your partner.                                                                               |
| `409 Conflict`          | An Auto-Earn preference change is already in progress for this account — only one may be in flight at a time                       | Poll `GET /v1/accounts/{account_id}/earn/auto` until no yield source is in `pending_enabled` / `pending_disabled`, then retry. |
| `429 Too Many Requests` | Rate limit hit                                                                                                                     | Honor `Retry-After` and back off exponentially.                                                                                |

### Best practices

1. **Treat the `PATCH` as eventually consistent.** A `200 OK` confirms the change was accepted, not that balances are already earning.
2. **Only one preference change in flight per account.** Wait for the pending state to resolve before submitting another change; otherwise the second `PATCH` returns `409 Conflict`.
3. **Only send the yield sources you actually want to change.** Omit the others to keep their preferences untouched.
4. **Pass `quote_symbol` consistently** across allocations, rewards, and Portfolio so converted totals line up across views.
5. **Don't assume APY is fixed.** Refetch the asset list whenever you display APY; it drifts with network conditions and country availability.
6. **Capture `Request-Id` from response headers.** It speeds up support investigations.

## API reference

| Endpoint                                          | Method | Description                                                                                                          |
| ------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| `/v1/earn/auto/assets`                            | GET    | List Auto-Earn assets eligible in a country (user-agnostic). Required `country` query parameter.                     |
| `/v1/accounts/{account_id}/earn/auto/assets`      | GET    | List Auto-Earn assets available to the given account, with APY and per-user cap.                                     |
| `/v1/accounts/{account_id}/earn/auto`             | PATCH  | Set Auto-Earn preferences per yield source. Async; empty `data: {}` on success.                                      |
| `/v1/accounts/{account_id}/earn/auto`             | GET    | Get current Auto-Earn preferences. Includes `pending_enabled` / `pending_disabled` while a `PATCH` is being applied. |
| `/v1/accounts/{account_id}/earn/auto/allocations` | GET    | List the user's current per-asset allocations and the cross-asset total in the requested quote currency.             |
| `/v1/accounts/{account_id}/earn/auto/rewards`     | GET    | List historical rewards per asset, the all-time total in the quote currency, and the next payout estimate.           |
