# API

> Base URLs, authentication, response format and the main endpoints of the Floatless API.

Source: https://floatless.xyz/docs/api

The Floatless API is the backend the Floatless app talks to. This page covers its two deployments, sessions, the response format, limits and the main routes.

> The API serves the Floatless app and doesn't issue API keys. Routes can change while the app is in private testing.

## Base URLs

| Network       | Base URL                            | Chain ID |
| ------------- | ----------------------------------- | -------- |
| Monad mainnet | `https://api-mainnet.floatless.xyz` | 143      |
| Monad testnet | `https://api.floatless.xyz`         | 10143    |

Each deployment has its own database, so records on one network never show up on the other. Floatless Savings is live on the mainnet API, and the testnet API returns a preview of it. Regulated dollars, the `/v1/compliance` routes, work on the testnet API only. Spending rules work on both. [Mainnet and testnet](https://floatless.xyz/docs/networks) covers the split for app users.

## Check the network

Every response carries an `x-floatless-chain-id` header with the chain ID the server runs on. The app refuses to use a server whose chain ID doesn't match its own network, and your client should do the same. Every response also carries an `x-request-id`, which error bodies repeat as `requestId`. You can send your own `X-Request-Id` to tag a request.

```sh
curl -i https://api-mainnet.floatless.xyz/health/ready
```

These are the headers that matter here.

```text
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
x-request-id: pDU-xw9EAmblDfKxWoN04
x-floatless-chain-id: 143
```

This is the body, with its `productEvidence` field left out.

```json
{
  "ready": true,
  "scope": "service_dependencies_only",
  "checks": {
    "database": "ready",
    "monadRpc": "ready",
    "chainId": 143,
    "expectedChainId": 143,
    "fundingMandateRegistry": "ready",
    "kuruRouter": "ready",
    "kuruMarketData": "ready",
    "cleanverse": "not_required",
    "morpho": "configured"
  }
}
```

`/health/ready` answers 200 when every dependency is ready and 503 when one isn't. It checks the database, the Monad RPC and chain ID, the spending-rules registry, the Kuru exchange's router and market data, and Cleanverse where regulated dollars are configured. As its `scope` field says, it checks dependencies only, so it doesn't prove payments work end to end. On testnet the same call returns chain ID 10143, `"cleanverse": "ready"` and `"morpho": "unconfigured"`.

## Sessions

Send a session token as a bearer token on every route that needs one.

```text
Authorization: Bearer <token>
```

How you get the token depends on how the account signs in.

### Passkey accounts

A passkey account proves it controls its wallet by signing a one-time message.

1. Call `POST /v1/account/session/challenge` with `{ "walletAddress": "0x..." }`. The response holds a `challenge`, the `message` to sign, and an `expiresAt` five minutes away.
2. Sign `message` with the wallet as a standard EIP-191 Ethereum signed message.
3. Call `POST /v1/account/session` with the `walletAddress`, the `challenge` and the `signature`. The response holds an `accessToken` that lasts one hour, so `expiresIn` is 3600.

Each challenge works once. When the token expires, repeat the three steps. A new passkey account registers the same way with `POST /v1/account/passkey/challenge` and `POST /v1/account/passkey`, which also takes the passkey's credential ID and returns a session.

### Email accounts

Email accounts sign in through Privy. The app sends its Privy access token as the bearer token, and the API checks it with Privy. The first time it sees a new Privy user, the API creates the Floatless account and an embedded wallet for it. From then on, the API signs for that wallet through Privy when the user confirms an action in the app. [Keys and security](https://floatless.xyz/docs/security) explains what that means for your money.

## Responses and errors

A successful response wraps its result in an envelope. Some routes that create a record answer 201 instead of 200.

```json
{ "success": true, "data": {} }
```

An error uses the same envelope with an `error` object instead.

```json
{
  "success": false,
  "error": {
    "code": "AUTHENTICATION_REQUIRED",
    "message": "A valid bearer session is required.",
    "requestId": "OzWUKy1zf0qvo_WaFX2NT"
  }
}
```

Branch on `code`. The `message` is written in plain words, so you can show it to people. A 422 `VALIDATION_ERROR` adds `details` with the field problems, and a declined regulated transfer adds `reasons`. Some errors also carry a top-level `data` with context.

A few routes don't use the envelope. `/health` and `/health/ready` return plain JSON, and `DELETE /v1/goals/:id` answers 204 with no body. When Cleanverse can't be used, the compliance config, registration link, deposit address and report routes answer 503 with `"success": false` and a `data` object whose `status` is `unavailable`.

| Status | Code                              | When it happens                                                     |
| ------ | --------------------------------- | ------------------------------------------------------------------- |
| 401    | `AUTHENTICATION_REQUIRED`         | The route needs a session and none was sent                         |
| 401    | `AUTHENTICATION_INVALID`          | The header isn't a bearer token, or the token is invalid or expired |
| 403    | `ACCOUNT_NOT_ACTIVE`              | The account behind the token isn't active                           |
| 422    | `VALIDATION_ERROR`                | The request body failed validation                                  |
| 428    | `IDEMPOTENCY_KEY_REQUIRED`        | The route needs an `Idempotency-Key` and didn't get a valid one     |
| 409    | `IDEMPOTENCY_KEY_REUSED`          | The key was already used with a different body                      |
| 409    | `IDEMPOTENCY_REQUEST_IN_PROGRESS` | The first request with this key is still running                    |
| 429    | `RATE_LIMITED`                    | Too many requests from your IP address                              |
| 500    | `INTERNAL_ERROR`                  | An unexpected failure on the server                                 |

## Idempotency keys

Routes that record payments, savings receipts or spending rules need an `Idempotency-Key` header, so a retry never records the same thing twice.

- `POST /v1/send/execute`
- `POST /v1/deposit`
- `POST /v1/earn/receipts`
- `POST /v1/safe-locks/receipts`
- `POST /v1/mandate/sign`
- `POST /v1/mandate/registration`

A key is 16 to 128 characters long. It starts with a letter or digit, and the rest can be letters, digits, `.`, `_`, `:` or `-`. Keys are scoped to your account and the route, and kept for 24 hours.

- Sending the same key and body again returns the first response. Field order in the JSON body doesn't matter.
- The first response is kept even when it's an error below 500, so after fixing a rejected request, send it with a new key.
- Sending the same key with a different body gets 409 `IDEMPOTENCY_KEY_REUSED`.
- If the first request failed with a server error, the key is released and you can retry with it.

The app derives each key from what it records, such as the transaction hash for a payment or `earn:<transaction hash>` for a savings receipt.

## Rate limits

Limits count requests per IP address over a one-minute window.

| Routes                                             | Requests per minute |
| -------------------------------------------------- | ------------------- |
| Routes under `/v1/account/`, which include sign-in | 20                  |
| `POST /v1/waitlist`                                | 5                   |
| Everything else, including `/health`               | 120                 |

Over the limit, the API answers 429 `RATE_LIMITED` with a `Retry-After` header that says how many seconds to wait. In a browser, only pages on the API's allowed origins can read its responses. Requests without an `Origin` header, such as from a server or curl, aren't affected.

## Endpoints

Paths are relative to the base URL. Routes need a session unless they're marked public.

### Status and sign-in

All of these are public.

| Route                                | What it does                                                            |
| ------------------------------------ | ----------------------------------------------------------------------- |
| `GET /health`                        | Liveness check that returns `{ "message": "OK" }`                       |
| `GET /health/ready`                  | Checks the service's dependencies, as described above                   |
| `POST /v1/account/passkey/challenge` | Returns a one-time message to sign before registering a passkey account |
| `POST /v1/account/passkey`           | Registers the passkey account and returns a session                     |
| `POST /v1/account/session/challenge` | Returns a one-time message to sign in to an existing passkey account    |
| `POST /v1/account/session`           | Exchanges the signed message for a one-hour session                     |

### Account, balances and history

| Route                            | What it does                                                                                          |
| -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `GET /v1/account`                | Your Floatless account                                                                                |
| `GET /v1/portfolio`              | The live onchain balance of each supported asset, plus your Floatless Savings position on mainnet     |
| `GET /v1/portfolio/availability` | Spendable balances only                                                                               |
| `GET /v1/activity`               | Your history, newest first, 50 entries a page. Pass `?before=<id>` with the last id for the next page |

### Spending rules and goals

| Route                           | What it does                                                                         |
| ------------------------------- | ------------------------------------------------------------------------------------ |
| `GET /v1/mandate`               | Your active spending rules, or `null`                                                |
| `POST /v1/mandate/preview`      | Turns proposed rules into the exact typed data to sign, with the next version number |
| `POST /v1/mandate/sign`         | Saves the rules you signed. Needs an `Idempotency-Key`                               |
| `POST /v1/mandate/registration` | Links the onchain registration after checking its event. Needs an `Idempotency-Key`  |
| `GET /v1/goals`                 | Your savings goals                                                                   |
| `POST /v1/goals`                | Creates a goal                                                                       |
| `PATCH /v1/goals/:id`           | Changes a goal                                                                       |
| `DELETE /v1/goals/:id`          | Archives a goal                                                                      |
| `GET /v1/goals/:id/history`     | A goal's change history                                                              |

### Payments

| Route                       | What it does                                                                                                                                                                                       |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/ens/resolve?name=` | Resolves an ENS name to `{ name, address, source }`. `source` is `monad`, `evm` or `ethereum`, the first record the name set in that order. Refuses a contract wallet that exists only on Ethereum |
| `POST /v1/send/quote`       | Runs every check before you sign and returns `ALLOW` or `DECLINE` with reasons. When allowed, it adds the gas limit, gas price and maximum network fee. A quote lasts up to 60 seconds             |
| `POST /v1/send/execute`     | Records a settled payment after verifying the transfer onchain. Needs an `Idempotency-Key`                                                                                                         |
| `GET /v1/send/:id`          | One recorded payment receipt                                                                                                                                                                       |
| `POST /v1/deposit`          | Records a transfer into your wallet after verifying it onchain. Needs an `Idempotency-Key`                                                                                                         |
| `GET /v1/proof/:receiptId`  | A receipt with its decision and its history entry                                                                                                                                                  |

### Floatless Savings

| Route                    | What it does                                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `GET /v1/earn`           | Your savings: `live` with your position on mainnet, `preview` on testnet, or `unavailable` with a reason     |
| `POST /v1/earn/receipts` | Records a savings deposit or withdrawal after checking the vault's event onchain. Needs an `Idempotency-Key` |

### Scheduled releases

| Route                          | What it does                                                                                                                                                                         |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST /v1/safe-locks/quote`    | Runs the payment checks for a release of `amount` AUSD to `beneficiary` and returns `ALLOW` or `DECLINE` with reasons, plus the SafeLock address. Nothing is signed                  |
| `GET /v1/safe-locks`           | The releases you created, newest first, with each one's live state on Monad: amounts in base units, `unlockAt`, `released`, `releasable`, and who released it when that's recorded   |
| `GET /v1/safe-locks/:lockId`   | One of your releases by its bytes32 ID                                                                                                                                               |
| `POST /v1/safe-locks/receipts` | Records a `create` or `release` after checking SafeLock's event onchain. A release by any wallet is accepted and attributed to Chainlink CRE or a wallet. Needs an `Idempotency-Key` |

These routes answer 503 `SAFE_LOCK_UNAVAILABLE` on a network without SafeLock, which today means testnet.

### Regulated dollars

These work on the testnet API only.

| Route                                    | What it does                                                                          |
| ---------------------------------------- | ------------------------------------------------------------------------------------- |
| `GET /v1/compliance/config`              | Public. Cleanverse's environment and the regulated assets it lists                    |
| `GET /v1/compliance/apass`               | Your wallet's Cleanverse identity status: `active`, `inactive` or `unavailable`       |
| `POST /v1/compliance/apass/magic-link`   | Your personal Cleanverse registration link                                            |
| `POST /v1/compliance/preflight`          | Runs the Cleanverse check for a transfer and records the decision                     |
| `GET /v1/compliance/deposit`             | Your deposit address for turning USDC into aUSDC                                      |
| `GET /v1/compliance/reports/:activityId` | A short-lived download link for the Travel Rule report of a settled regulated payment |

```sh
curl https://api.floatless.xyz/v1/compliance/config
```

```json
{
  "success": true,
  "data": {
    "status": "available",
    "environment": "uat",
    "enforcement": "configured",
    "chain": "monad",
    "assets": [
      {
        "symbol": "ausdc",
        "address": "0xaC0893567D43C3E7e6e35a72803df05416C1f20D",
        "decimals": 6,
        "origin": {
          "symbol": "usdc",
          "address": "0x534b2f3A21130d7a60830c2Df862319e593943A3",
          "decimals": 6
        },
        "accessCore": "0x8F118338a1fa41E7Fa86Be19A4e8B99Ed58A6EcC",
        "apassAddress": "0xbA82D189540CaC9DC6FF46B6837CaC1BFdEC58B9"
      }
    ]
  }
}
```

The API also has routes the app uses internally, such as embedded-wallet signing under `/v1/wallet`, and routes for experimental features. They aren't covered here and aren't meant for other clients.

To ask questions about Floatless from an AI assistant, see [Use Floatless in ChatGPT and Claude](https://floatless.xyz/docs/ai-assistants).
