# Create Transaction

Send a native coin or a token from a wallet the authenticated user owns.

## Overview

This is the send endpoint for every wallet type. It stores a transaction record and then either signs and submits it for you (custodial wallets) or returns the hash your client must sign (non-custodial wallets). Which path runs depends on the wallet's `keyManagementScheme` — see **Custody Paths** below.

- **Recipient:** pass exactly one of `toAddress` (an EVM `0x` address or a Solana base58 address) or `toUsername` (a user in your tenant).
- **Chain:** `chain` defaults to the wallet's own chain. An EVM address is the same on every EVM network, so you can pass a different EVM chain to send from the same address there.
- **Token:** set `tokenAddress` to the token contract (EVM) or mint (Solana). Omit it to send the native coin.

> ⚠️ `amount` is an integer string in the token's smallest unit: `"10000000000000000"` is 0.01 of an 18-decimal coin, `"25000000"` is 25 of a 6-decimal token. A decimal such as `"0.01"` fails schema validation with `400`. See [Amount Units](https://docs.aureahub.com/docs/amounts.md).

## Endpoint

### `POST /v1/transactions/`

Authentication: bearer token required.

Creates a send from a wallet owned by the authenticated user.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `walletId` | string (uuid) | yes | Source wallet. Must belong to the authenticated user. |
| `amount` | string | yes | Integer in the smallest unit (digits only). Must be greater than 0 and at most 1030. |
| `toAddress` | string | no | Recipient address: `0x` followed by 40 hex characters, or a base58 string of 32–44 characters. Provide this or `toUsername`, not both. |
| `toUsername` | string | no | Recipient username in your tenant (3–100 characters: letters, digits, `_`, `-`). Resolved to one of the recipient's wallets on the same network type (mainnet or testnet) and chain family (Solana or EVM), preferring their primary wallet. |
| `chain` | string | no | Chain identifier. Defaults to the wallet's chain. |
| `tokenAddress` | string | no | Token contract (EVM) or mint (Solana) address. Omit for the native coin. |
| `gasLimit` | string | no | Gas limit (digits only). For non-custodial sends it replaces the server's estimate. |
| `gasPrice` | string | no | Gas price in wei (digits only). Passed on only for custodial sends. |
| `gasless` | boolean | no | Accepted by the schema (default `false`). The server decides whether a send is gasless — see Custody Paths. |

**Responses**

`201` Created (custodial)

```json
{
  "id": "0b9d4c1e-3f6a-4b8e-9d2c-7a5e1f3b6c90",
  "txHash": "0x7f3a…",
  "chain": "gnosis",
  "type": "send",
  "status": "pending",
  "fromAddress": "0x2f4B…",
  "toAddress": "0x52908400098527886E0F7030069857D2E4169EE7",
  "token": { "address": null, "symbol": "xDAI", "name": null, "decimals": 18 },
  "amount": "10000000000000000",
  "amountUsd": null,
  "amountEur": null,
  "fee": { "amount": null, "usd": null },
  "gasUsed": null,
  "gasPrice": null,
  "blockNumber": null,
  "blockTimestamp": null,
  "createdAt": "2026-09-11T10:00:00.000Z",
  "updatedAt": "2026-09-11T10:00:01.000Z"
}
```

`201` Created (non-custodial)

```json
{
  "id": "0b9d4c1e-3f6a-4b8e-9d2c-7a5e1f3b6c90",
  "txHash": null,
  "status": "pending",
  "…": "other transaction fields as in the custodial example",
  "requiresClientSigning": true,
  "gasless": false,
  "preparedTxId": "5e0f7b8a-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
  "txHashToSign": "0x4a7d…",
  "nonce": "12",
  "maxFeePerGas": "3000000000",
  "maxPriorityFeePerGas": "1500000000",
  "chainId": 100,
  "expiresAt": "2026-09-11T10:05:00.000Z"
}
```

`201` Created (gasless)

```json
{
  "id": "0b9d4c1e-3f6a-4b8e-9d2c-7a5e1f3b6c90",
  "txHash": null,
  "status": "pending",
  "…": "other transaction fields as in the custodial example",
  "requiresClientSigning": true,
  "gasless": true,
  "preparedTxId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "permitHashToSign": "0xc19e…",
  "nonce": "0",
  "expiresAt": "2026-09-11T10:05:00.000Z"
}
```

`400` Bad Request

```json
{ "error": "Bad Request", "message": "Request validation failed" }
```

`400` Bad Request

```json
{ "error": "BadRequestError", "message": "Non-custodial Solana sends are not yet supported" }
```

`404` Not Found

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

`422` Validation Error

```json
{
  "statusCode": 422,
  "error": "ValidationError",
  "message": "Validation failed",
  "details": [
    { "code": "custom", "message": "Must provide either toAddress OR toUsername (not both)", "path": [] }
  ]
}
```

**Example request**

```bash
curl -X POST https://api.aureahub.com/v1/transactions/ \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "walletId": "3f1c2b9e-8a4d-4c6e-9b2a-5d7e1f0a6c3b",
    "chain": "gnosis",
    "toAddress": "0x52908400098527886E0F7030069857D2E4169EE7",
    "amount": "10000000000000000"
  }'
```

## Custody Paths

| keyManagementScheme | What this endpoint does |
| --- | --- |
| `aes_single`, `sss_2of2_akv` | **Custodial.** Aurea signs and submits the transaction in the same call. EVM sends return `status: "pending"` with the on-chain `txHash`. Solana sends wait for confirmation and return `status: "confirmed"`. If submission fails, the record is saved as `failed` and the call returns `400` with `Failed to send transaction: …`. |
| `client_side`, `client_side_pending`, `mpc_tss` | **Non-custodial, EVM only.** Aurea saves a `pending` record and returns `requiresClientSigning: true`, `txHash: null` and a `preparedTxId` that expires 5 minutes later (`expiresAt`). Sign and submit it with [Broadcast Transaction](https://docs.aureahub.com/docs/tx-broadcast.md). Solana wallets get `400` `Non-custodial Solana sends are not yet supported`. |

### Standard non-custodial send (`gasless: false`)

- `txHashToSign` is the hash of an unsigned EIP-1559 transaction built and stored by the API. Sign it with the wallet key and broadcast with `v` = 0 or 1.
- `maxFeePerGas` and `maxPriorityFeePerGas` are the network's current fee data multiplied by 1.5.
- Gas limit: your `gasLimit` if given; otherwise 21000 for a native coin, and for a token the network estimate plus 30% (150000 if the estimate fails).
- The numeric chain ID is looked up in the chain registry (or your tenant's enabled blockchain config). If none is found the call returns `400` `Chain "…" (isTestnet=…) is not registered in the chain registry`.

### Gasless token send (`gasless: true`)

The API picks this path automatically when `tokenAddress` is set, the chain has a gasless transfer forwarder configured, and the token is registered as active with permit (EIP-2612) support. The response carries `permitHashToSign`, an EIP-712 digest, instead of `txHashToSign`. Sign it and broadcast with `v` = 27 or 28; a relayer then submits the transfer. Which chains and tokens qualify depends on the deployment's configuration.

## Errors

For `400`, `401` and `404` the body is `{ "error", "message" }`. See [Error Handling](https://docs.aureahub.com/docs/errors.md).

| Status | When |
| --- | --- |
| 400 | Schema validation failed (`Request validation failed`) — for example a decimal `amount`, a malformed address or a non-UUID `walletId`. Also: non-custodial Solana send; chain not found for a non-custodial send; token metadata could not be read (`Invalid token address or unable to fetch token information`); custodial submission failed (`Failed to send transaction: …`). |
| 401 | Missing or invalid bearer token. |
| 404 | `Wallet not found` (missing, or not owned by the caller); `User with username "…" not found`; `Recipient "…" does not have a … wallet`. |
| 422 | Cross-field rules: neither or both of `toAddress` / `toUsername`; `amount` of 0; `amount` above 1030. The body includes `details`. |

## Implementation

Token decimals are listed by `GET /v1/tokens/chain/{chain}`. Convert the human amount before sending, then branch on `requiresClientSigning`:

```javascript
const API = 'https://api.aureahub.com';

// "1.5" with 6 decimals -> "1500000". Amount Units has a fuller helper.
function toSmallest(amount, decimals) {
  const [whole, frac = ''] = String(amount).split('.');
  const fraction = (frac + '0'.repeat(decimals)).slice(0, decimals);
  return (BigInt(whole || '0') * 10n ** BigInt(decimals) + BigInt(fraction || '0')).toString();
}

async function createSend(token, { walletId, chain, toAddress, amount, decimals, tokenAddress }) {
  const res = await fetch(API + '/v1/transactions/', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      walletId,
      chain,
      toAddress,
      amount: toSmallest(amount, decimals),
      ...(tokenAddress ? { tokenAddress } : {}),
    }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(res.status + ' ' + body.message);
  return body;
}

const tx = await createSend(token, {
  walletId, chain: 'gnosis', toAddress, amount: '0.01', decimals: 18,
});

if (tx.requiresClientSigning) {
  // Non-custodial: sign tx.txHashToSign (or tx.permitHashToSign when tx.gasless is true)
  // and call POST /v1/transactions/{id}/broadcast before tx.expiresAt.
} else {
  // Custodial: poll GET /v1/transactions/{id}/status until confirmed or failed.
}
```

---

Web version: https://docs.aureahub.com/#tx-create
