# Approve ERC-20 Token

Grant the swap spender — the quote's `approvalAddress` — an ERC-20 allowance on the source token.

## Overview

This endpoint builds an ERC-20 `approve(spenderAddress, amount)` call on `tokenAddress` from the wallet `walletId`. `amount` is an integer string in the token's smallest unit (pattern `^\d+$`). When it is omitted, the approval is for the maximum `uint256` value, i.e. unlimited.

Call it when [Get Quote](https://docs.aureahub.com/docs/swap-quote.md) returns `needsApproval: true`. Use the quote's `approvalAddress` as `spenderAddress` and the chain you quoted on as `chain`; without `chain`, the wallet's own chain is used. The `gasless` field is accepted but not used by this endpoint.

> ℹ️ Solana has nothing to approve. When the target chain is `solana`, the API returns `{ "txHash": "not_applicable", "status": "confirmed", "approvedAmount": "not_applicable", "gasUsed": "0" }` without sending a transaction.

## Custodial vs Non-Custodial

**Custodial wallets** (Aurea holds the key): the API signs and sends the approval, **waits for the receipt**, and returns `txHash`, `status` (`confirmed` or `failed`), `approvedAmount` and `gasUsed`. Once the response shows `confirmed` you can execute the swap.

**Non-custodial wallets** (key management scheme `client_side`, `client_side_pending` or `mpc_tss`): nothing is sent yet. The API prepares an unsigned EIP-1559 transaction, with fee caps set to 1.5× the network's current fee data. It returns `requiresClientSigning: true` with `preparedTxId`, `txHashToSign`, `chainId`, `nonce`, `maxFeePerGas`, `maxPriorityFeePerGas` and `expiresAt`. Sign `txHashToSign` with the wallet key and send `{ preparedTxId, v, r, s }` to [Broadcast Approve Tx](https://docs.aureahub.com/docs/swap-broadcast-approve.md) before `expiresAt` (5 minutes).

> ⚠️ For non-custodial wallets the API does not check `spenderAddress` against the quote. Always pass the quote's `approvalAddress`.

## Endpoint

### `POST /v1/swap/approve`

Authentication: bearer token required.

Approves an ERC-20 spender. Custodial wallets: sent and confirmed. Non-custodial wallets: returns a transaction hash to sign.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `walletId` | string (uuid) | yes | Wallet that owns the tokens. Must belong to the authenticated user. |
| `tokenAddress` | string | yes | ERC-20 token contract: `0x` + 40 hex characters (the schema also accepts a Solana base58 address). |
| `spenderAddress` | string | yes | Spender, `0x` + 40 hex characters — the quote's `approvalAddress`. |
| `amount` | string | no | Allowance in the token's smallest unit, digits only (`^\d+$`). Omit for an unlimited approval. |
| `chain` | string | no | Chain to approve on. Defaults to the wallet's chain. |
| `isTestnet` | boolean | no | Use the testnet of the chain. Defaults to `false`. |
| `gasless` | boolean | no | Accepted; not used by this endpoint. |

**Responses**

`200` Custodial

```json
{
  "txHash": "0x…",
  "status": "confirmed",
  "approvedAmount": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
  "gasUsed": "…"
}
```

`200` Non-custodial

```json
{
  "requiresClientSigning": true,
  "preparedTxId": "…",
  "txHashToSign": "0x…",
  "chainId": 137,
  "nonce": "…",
  "maxFeePerGas": "…",
  "maxPriorityFeePerGas": "…",
  "expiresAt": "2026-09-11T10:05:00.000Z"
}
```

`400` Bad Request

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "Cannot approve from watch-only wallet"
}
```

`404` Not Found

```json
{
  "statusCode": 404,
  "error": "NotFoundError",
  "message": "Wallet not found"
}
```

Other `400` responses include `Request validation failed`, `Failed to approve token: …` (custodial), `Network mismatch: …`, and for non-custodial wallets `Token contract rejected approval: check token address and spender`, `Cannot determine EIP-1559 gas fees for this network` and `Chain "…" (isTestnet=…) is not registered in the chain registry`.

## Implementation

```javascript
import { SigningKey } from 'ethers';

// Signs a prepared transaction hash; v must be the recovery parity (0 or 1)
const signDigest = async (digest) => {
  const sig = new SigningKey(privateKey).sign(digest); // or your MPC / device signer
  return { v: sig.yParity, r: sig.r, s: sig.s };
};

async function approveForSwap(token, { walletId, tokenAddress, spenderAddress, chain, amount }) {
  const post = async (path, body) => {
    const res = await fetch(`https://api.aureahub.com${path}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
      body: JSON.stringify(body)
    });
    const json = await res.json();
    if (!res.ok) throw new Error(`${res.status} ${json.code ?? ''}: ${json.message}`);
    return json;
  };

  // amount omitted → unlimited allowance
  const result = await post('/v1/swap/approve', { walletId, tokenAddress, spenderAddress, chain, amount });

  if (!result.requiresClientSigning) {
    // Custodial: the approval has already been mined
    if (result.status !== 'confirmed') throw new Error('Approval transaction failed');
    return result.txHash;
  }

  // Non-custodial: sign txHashToSign and broadcast within 5 minutes
  const { v, r, s } = await signDigest(result.txHashToSign);
  const { txHash } = await post('/v1/swap/broadcast-approve', { preparedTxId: result.preparedTxId, v, r, s });

  // Approvals have no transaction record: poll /v1/swap/check-allowance until it is in place
  return txHash;
}
```

---

Web version: https://docs.aureahub.com/#swap-approve
