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

# Approving transfers without gas

> How self-custody accounts let Venly move their tokens — permits, allowances, and wallet activation.

Applies to **self-custody** accounts. (New to that term? See [Venly-managed vs self-custody](/guides/finance/managed-vs-self-custody).) Do this **after** the account is [verified](/guides/finance/kyc-verification) and its wallet's AML check is approved.

## Why an allowance is needed

A self-custody account holder controls their own wallet. For Venly to move that wallet's tokens during a payment or transfer, the wallet must grant Venly's **orchestration wallet** an ERC-20 **allowance** — permission to pull funds via `transferFrom`. Supported stablecoins are **USDC, EURC, USDS, and USDT** (all support gasless permits).

## Two wallets, two responsibilities

Each account has **two** wallets, and each needs an allowance to the orchestration wallet:

| Wallet                                            | Who grants the permit                                                                                         |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Account wallet** (holds the customer's balance) | **You** — the account holder signs it (self-custody). On Venly-managed accounts Venly signs it automatically. |
| **Escrow wallet** (used during settlement)        | **Venly, automatically** — the escrow wallet is always Venly-managed.                                         |

So on a self-custody account the **only** permit you sign is the **account wallet's**. The escrow wallet — and, on Venly-managed accounts, the account wallet too — is permitted for you automatically.

## Why a permit instead of a plain `approve`

A normal `approve()` costs gas and needs the wallet to hold the chain's native coin. Instead, the holder **signs** an EIP-2612 message (off-chain, no gas) and Venly's orchestration wallet submits it on-chain and pays the gas.

<Note>
  A direct on-chain `approve(spender, amount)` *does* set the allowance — but only a **confirmed permit** moves the wallet to `ACTIVE` (see below). An `approve` alone leaves the wallet `PENDING`, so payments stay blocked. Use the permit flow.
</Note>

## The permit flow (self-custody account wallet)

<Steps>
  <Step title="Get the message to sign">
    `GET /accounts/{accountId}/wallets/{walletId}/permits` returns, per asset, a `supportedAssetId` and an EIP-712 `typedData` object. `walletId` sets the chain (e.g. Base vs Avalanche). The nonce and the token's domain `name`/`version` are read from the contract for you.
  </Step>

  <Step title="Sign it with the owner's key">
    Sign `typedData` with the key for the wallet's **owner** address. **The signature must recover to the `owner`** — a signature from any other wallet makes the permit `FAILED`. Produces `v`, `r`, `s`.
  </Step>

  <Step title="Submit the signature">
    `POST .../permits` with the `supportedAssetId` and the signature. Returns **HTTP 200 with `result.status`** — check the status, not just the code. Re-submitting an already-confirmed permit returns `409`.
  </Step>

  <Step title="Wait for CONFIRMED">
    Settlement is asynchronous — poll `GET .../permits` until `CONFIRMED` (or `FAILED`). On `CONFIRMED` the wallet activates and the allowance goes live.
  </Step>
</Steps>

### Permit status lifecycle

```
PENDING ──submit──▶ SUBMITTED ──on-chain confirmed──▶ CONFIRMED
   │                    │
   └────────── FAILED ◀─┘
```

| `status`    | Meaning                                                                                                                                                 |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PENDING`   | Not yet submitted                                                                                                                                       |
| `SUBMITTED` | Signature accepted; settling on-chain                                                                                                                   |
| `CONFIRMED` | Confirmed on-chain — triggers wallet activation                                                                                                         |
| `FAILED`    | Execution/confirmation failed — most often the signature didn't recover to the wallet `owner`. Re-fetch the permit and sign again with the correct key. |

### Wallet status lifecycle

A wallet starts `PENDING` and becomes **`ACTIVE` only once *all* its permits are `CONFIRMED`**:

```
PENDING ──all permits CONFIRMED──▶ ACTIVE
```

**Payments and transfers require the wallet to be `ACTIVE`** — until then they're rejected with `account-wallet-not-active`.

<Note>
  The wallet's `status` isn't currently exposed by `GET .../wallets` (which shows `amlStatus` only) — use the **permit status** as your activation signal: once the account wallet's permit is `CONFIRMED`, the wallet is `ACTIVE`.
</Note>

## Signing the message

Take the `typedData` from the permit response and sign it with the **owner's** key. With [ethers.js](https://docs.ethers.org):

```javascript theme={null}
import { ethers } from "ethers";

// `typedData` is result[].typedData from GET .../permits
const { domain, types, message } = typedData;

// ethers builds the domain separator itself, so remove EIP712Domain before signing
const { EIP712Domain, ...signTypes } = types;

const wallet = new ethers.Wallet(PRIVATE_KEY); // MUST be the wallet `owner` key
const signature = await wallet.signTypedData(domain, signTypes, message);

const { v, r, s } = ethers.Signature.from(signature); // submit these below
```

<Warning>
  The signature **must recover to the wallet `owner`**. Signing with a different wallet (or signing the JSON as a personal message instead of EIP-712 typed data) produces a signature that the token contract rejects — the permit silently becomes `FAILED`.
</Warning>

Then submit it. Send `v`, `r`, and `s` as **strings** in the `signature` body — `ethers` returns `v` as a number, so convert it (`String(v)`). Use the `supportedAssetId` from the `GET .../permits` response:

```bash theme={null}
curl --request POST \
  --url https://api.venlyfinance.com/v1/accounts/{accountId}/wallets/{walletId}/permits \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '{
    "supportedAssetId": "7c32877a-cd0c-422a-a0fd-5af0815fc280",
    "signature": { "v": "27", "r": "0x...", "s": "0x..." }
  }'
```

## Verifying the allowance

Before initiating a payment or transfer, confirm the allowance is in place:

```bash theme={null}
curl https://api.venlyfinance.com/v1/accounts/{accountId}/wallets/{walletId}/allowances \
  -H "Authorization: Bearer <token>"
```

Each entry returns the asset, the `orchestrationWallet` that holds the allowance, and the `allowance` as a **human-readable decimal** (on-chain raw value ÷ 10^`decimals`). The authoritative activation signal is the **permit `status: CONFIRMED`** (above) — not the allowance figure. An unlimited permit reports a very large `allowance` (uint256-max), but the value can also be finite; use this endpoint to read the current spending headroom and the permit status to confirm the wallet is active.

<Note>
  Once a permit is `CONFIRMED`, the allowance persists until it's used up — the holder doesn't sign again for every transfer.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Get wallet allowances" icon="list-check" href="/api-reference/Finance-API/allowances/get-wallet-token-allowances">
    Confirm the allowance is live before moving funds.
  </Card>

  <Card title="Create a crypto transfer" icon="code" href="/api-reference/Finance-API/transfers/create-crypto-transfer">
    Move funds once the wallet is `ACTIVE`.
  </Card>
</CardGroup>
