# Sending a Transaction

Custodial, non-custodial and gasless sends — one endpoint to start, and at most one more to finish.

## Overview

Every send starts with `POST /v1/transactions/`. For a custodial wallet that single call signs and submits. For a non-custodial wallet it returns a hash to sign, and a second call submits the signature. Your code does not choose the path: branch on `requiresClientSigning` (and `gasless`) in the response.

```text
Custodial      POST /v1/transactions/ ──► poll GET /v1/transactions/{id}/status

Non-custodial  POST /v1/transactions/ ──► sign txHashToSign or permitHashToSign
               ──► POST /v1/transactions/{id}/broadcast ──► poll GET /v1/transactions/{id}/status
```

## Before You Send

- **Amounts** are integer strings in the token's smallest unit. Get token addresses and decimals from `GET /v1/tokens/chain/{chain}` and convert — see [Amount Units](https://docs.aureahub.com/docs/amounts.md).
- **Custody** is given by the wallet's `keyManagementScheme`: `aes_single` and `sss_2of2_akv` are custodial; `client_side`, `client_side_pending` and `mpc_tss` are non-custodial.
- **Fees:** [Transaction Quote](https://docs.aureahub.com/docs/tx-quote.md) gives an estimate before you send.

```typescript
const API = 'https://api.aureahub.com';
const auth = { Authorization: 'Bearer ' + token };

const { tokens } = await fetch(API + '/v1/tokens/chain/gnosis', { headers: auth }).then(r => r.json());
const usdc = tokens.find(t => t.symbol === 'USDC'); // { symbol, name, address, decimals, isNative, ... }
const amount = toSmallest('25', usdc.decimals);     // toSmallest: see Amount Units
```

## Custodial

```typescript
const tx = await fetch(API + '/v1/transactions/', {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    walletId,
    chain: 'gnosis',
    toAddress,               // or toUsername: 'bob' (exactly one of the two)
    amount,                  // smallest unit
    tokenAddress: usdc.address,
  }),
}).then(r => r.json());

// EVM: tx.status === 'pending' and tx.txHash is set. Poll the status endpoint next.
```

If the submission fails, the record is saved as `failed` and the call returns `400` with `Failed to send transaction: …`.

## Non-Custodial (EVM)

1. `POST /v1/transactions/` stores a `pending` record, builds an unsigned EIP-1559 transaction and returns `requiresClientSigning: true`, `preparedTxId`, `txHashToSign` and `expiresAt`.
2. Sign `txHashToSign` — the 32-byte hash itself, without an EIP-191 prefix.
3. Within 5 minutes, send `preparedTxId`, `v` (0 or 1), `r` and `s` to `POST /v1/transactions/{id}/broadcast`. The API checks that the signature recovers to the wallet address and submits the transaction.

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

async function sendNonCustodial(walletId: string, toAddress: string, amount: string, privateKey: string) {
  // 1. Create: the API builds and stores the unsigned transaction
  const tx = await fetch(API + '/v1/transactions/', {
    method: 'POST',
    headers: { ...auth, 'Content-Type': 'application/json' },
    body: JSON.stringify({ walletId, chain: 'gnosis', toAddress, amount }),
  }).then(r => r.json());

  if (!tx.requiresClientSigning) return tx; // custodial wallet: already submitted

  // 2. Sign on the client
  const key = new SigningKey(privateKey);
  const sig = key.sign(tx.gasless ? tx.permitHashToSign : tx.txHashToSign);
  const v = tx.gasless ? sig.v : sig.yParity; // 27|28 for gasless permits, 0|1 otherwise

  // 3. Broadcast before tx.expiresAt
  const res = await fetch(API + '/v1/transactions/' + tx.id + '/broadcast', {
    method: 'POST',
    headers: { ...auth, 'Content-Type': 'application/json' },
    body: JSON.stringify({ preparedTxId: tx.preparedTxId, v, r: sig.r, s: sig.s }),
  });
  const sent = await res.json();
  if (!res.ok) throw new Error((sent.code ?? res.status) + ': ' + sent.message);
  return sent; // status 'pending', txHash = on-chain hash
}
```

- Fees are set by the server: `maxFeePerGas` and `maxPriorityFeePerGas` are the network's fee data × 1.5. Gas limit is your `gasLimit`, or 21000 for a native coin, or the network estimate + 30% for a token (150000 if estimation fails).
- `mpc_tss` wallets produce the signature through the 2-of-3 MPC ceremony instead of a single local key; the broadcast body is the same.
- Solana is not supported on this path (`400`).

## Gasless Token Sends

For a non-custodial token send, the API switches to the gasless path by itself when the chain has a gasless transfer forwarder configured and the token is registered as active with permit (EIP-2612) support. The response then has `gasless: true` and `permitHashToSign` (an EIP-712 digest) instead of `txHashToSign`.

- Sign `permitHashToSign` and broadcast with `v` = 27 or 28 to the same `POST /v1/transactions/{id}/broadcast` — the code above already handles both cases.
- A relayer submits the permit-and-transfer transaction and waits for one confirmation; the response is the record with `status: "pending"` and the on-chain `txHash`.
- If the relayer fails, the broadcast returns `500` with code `TX_SUBMISSION_FAILED` and `retryable: true`.
- Which chains and tokens qualify depends on your deployment's configuration.

## Solana

Solana sends (`chain: "solana"`) are supported for **custodial** wallets only. Send SOL by omitting `tokenAddress`, or an SPL token by setting `tokenAddress` to its mint. The API waits for confirmation before responding, so the returned record already has `status: "confirmed"`.

```typescript
const solTx = await fetch(API + '/v1/transactions/', {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    walletId: solanaWalletId,
    chain: 'solana',
    toAddress: solanaRecipient,   // base58 address
    amount: '1000000',            // smallest unit
  }),
}).then(r => r.json());
// solTx.status === 'confirmed'
```

A non-custodial Solana wallet gets `400` `Non-custodial Solana sends are not yet supported`. See [Supported Blockchains](https://docs.aureahub.com/docs/supported-blockchains.md) for the full feature matrix.

## Tracking Status

Poll [Get Tx Status](https://docs.aureahub.com/docs/tx-status.md) until `status` is `confirmed` or `failed`. It checks the EVM receipt and saves any change; a background job also advances pending EVM transactions every 60 seconds. For an activity screen that mixes blockchain and fiat items, use the [Unified Transaction Feed](https://docs.aureahub.com/docs/agg-tx-list.md).

## Retries and Expiry

- The transaction endpoints do not support idempotency keys. Every `POST /v1/transactions/` that passes validation stores a new record, and for custodial wallets submits a new transaction. After a timeout, check [List Transactions](https://docs.aureahub.com/docs/tx-list.md) (newest first) before retrying.
- A prepared send can be broadcast once — a second attempt returns `409` `PREPARED_TX_ALREADY_USED`.
- After 5 minutes the broadcast returns `410` `PREPARED_TX_EXPIRED`. Create a new send; the expired record stays `pending` with no on-chain hash.
- `422` `WRONG_SIGNER` means the signature does not recover to the wallet address — check the key and `v`.

---

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