---
title: "Give a customer a stablecoin address on an EVM network"
description: "Create a Polygon (or Ethereum, Avalanche) address for a customer, see USDC or USDT arrive, and find the deposit in your ledger."
---
This recipe creates a Polygon, Ethereum or Avalanche address for a customer, shows USDC or USDT arriving, and finds the deposit in your ledger. It is for a remittance app whose users top up their balance by sending USDC from an exchange.

**Status.** Available for USDC and USDT on `polygon`, `ethereum` and `avalanche`.

## Money example

1. Ada asks for a deposit address. You create a Polygon wallet for her with the address `0xc47B2D22812ee674Ee5182320FCCA83854f21962`.
2. Ada withdraws 250 USDC from her exchange, picks the **Polygon** network and pastes that address.
3. Gravv sees 250 USDC arrive, records a `crypto_funding` row of 250 for Ada, and sends you `inflow.onchain.completed`.
4. Her wallet shows `USDC 250.000000`.

## Before you start

- The customer exists, and [`GET /v1/customers/{customer_id}`](/api-reference/customers/get-v1-customers-customer-id) returns 200 for its id. Gravv does not check `customer_id` when it creates a wallet, so a typo creates a wallet that belongs to no customer.
- A scoped key needs `account:create`.
- If you want the deposit webhook, register your webhook URL in the Dashboard.

## Find out what is required

No API call lists the networks and coins for wallets. Pick a network and coin that the sender's exchange supports from [Supported networks and coins](/platform/wallets/overview#supported-networks-and-coins).

| `network` | USDC | USDT |
| :-- | :-- | :-- |
| `polygon`, `ethereum`, `avalanche` | Available | Available |

## Steps

### 1. Create the wallet

Each call with a new `Idempotency-Key` creates a new wallet. Keep the key with your own record, and reuse it only if you retry after a timeout.

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/wallets \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: eacdd086-e68e-479c-be3c-f7ba46e13aba' \
     --header 'Content-Type: application/json' \
     --data '
{
  "customer_id": "401d68fb-fe97-4c56-84a6-5651cdbde747",
  "network": "polygon",
  "name": "Ada top-up wallet"
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/wallets", {
  method: "POST",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Idempotency-Key": "eacdd086-e68e-479c-be3c-f7ba46e13aba",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "customer_id": "401d68fb-fe97-4c56-84a6-5651cdbde747",
    "network": "polygon",
    "name": "Ada top-up wallet"
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.post(
    "https://api.gravv.xyz/v1/wallets",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Idempotency-Key": "eacdd086-e68e-479c-be3c-f7ba46e13aba",
        "Content-Type": "application/json",
    },
    json={
        "customer_id": "401d68fb-fe97-4c56-84a6-5651cdbde747",
        "network": "polygon",
        "name": "Ada top-up wallet",
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "id": "bbab27f6-04b8-457d-83ed-b92009758340",
    "address": "0xc47B2D22812ee674Ee5182320FCCA83854f21962",
    "network": "polygon",
    "name": "Ada top-up wallet",
    "date_created": "2026-10-13T09:14:03Z",
    "customer_id": "401d68fb-fe97-4c56-84a6-5651cdbde747",
    "wallet_type": "customer"
  },
  "error": null
}
```

The response includes the address, so there is nothing to poll. Store `id` and `address` exactly as returned. EVM addresses come back in mixed (checksum) case, and the `wallet_address` filter matches case-sensitively. Show Ada the address **with the network**, for example "Polygon only". Gravv sends no webhook when it creates the wallet.

### 2. Wait for the deposit

When the coins arrive, Gravv sends `inflow.onchain.completed`.

```json title="inflow.onchain.completed"
{
  "tenant_id": "d1a4c01e-a887-4e22-9b35-411b72723b9c",
  "transaction_id": "polygon:0x6bebb2737f6a6f0fb23c6f5da2cec255404e4fb440034d6608697a8d41bed440",
  "event_category": "inflow",
  "event_type": "inflow.onchain.completed",
  "event_group_id": "64a149f5-e383-4b9e-95a9-422a8bc08311",
  "timestamp": "2026-10-13T09:31:12Z",
  "event_data": {
    "amount": "250.00",
    "asset": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
    "asset_symbol": "USDC",
    "blockchain_network": "polygon",
    "confirmations": 12,
    "destination": {
      "blockchain_memo": null,
      "blockchain_network": "polygon",
      "id": "bbab27f6-04b8-457d-83ed-b92009758340",
      "wallet_address": "0xc47B2D22812ee674Ee5182320FCCA83854f21962"
    },
    "remark": "Crypto Funding",
    "source": {
      "blockchain_memo": null,
      "blockchain_network": "polygon",
      "wallet_address": "0x9e9cb0eb53f16947ccf25ec84d8dbc74254770f5"
    },
    "status": "COMPLETED",
    "transaction_id": "polygon:0x6bebb2737f6a6f0fb23c6f5da2cec255404e4fb440034d6608697a8d41bed440",
    "tx_hash": "0x6bebb2737f6a6f0fb23c6f5da2cec255404e4fb440034d6608697a8d41bed440"
  },
  "event_id": "b00fd7bb-4eca-4ea2-81b6-2bb5f86664ae"
}
```

Check before you credit Ada in your app:

- The coin is USDC or USDT. Read `asset_symbol` when it is present, because `asset` can be the token's contract address.
- `blockchain_network` is the wallet's network.
- Gravv also sends this event for other tokens that land on the address, such as a spam token. Gravv records only USDC and USDT on the networks above as deposits.
- `transaction_id` here is `<network>:<hash>`, not the id of a transaction row. Find the row with `tx_hash` in step 4.

### 3. Read the balance

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/wallets/bbab27f6-04b8-457d-83ed-b92009758340 \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/wallets/bbab27f6-04b8-457d-83ed-b92009758340", {
  method: "GET",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
  },
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.get(
    "https://api.gravv.xyz/v1/wallets/bbab27f6-04b8-457d-83ed-b92009758340",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response (trimmed)"
{
  "data": {
    "id": "bbab27f6-04b8-457d-83ed-b92009758340",
    "address": "0xc47B2D22812ee674Ee5182320FCCA83854f21962",
    "network": "polygon",
    "assets": [
      { "symbol": "USDC", "balance": "250.000000", "decimals": 6, "updated_at": "2026-10-13T09:31:20Z" },
      { "symbol": "USDT", "balance": "0.000000", "decimals": 6, "updated_at": "2026-10-13T09:31:20Z" },
      { "symbol": "POL", "balance": "0.000000000000000000", "decimals": 18, "updated_at": "2026-10-13T09:31:20Z" }
    ]
  },
  "error": null
}
```

Gravv caches balances. A missing coin means "not read yet", not zero. `"stale": true` means Gravv has queued a refresh, so read again a few seconds later.

### 4. Find the row in your ledger

The `tx_hash` filter needs an exact match, so send the hash exactly as the event shows it.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v1/transactions?customer_id=401d68fb-fe97-4c56-84a6-5651cdbde747&tx_hash=0x6bebb2737f6a6f0fb23c6f5da2cec255404e4fb440034d6608697a8d41bed440' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/transactions?customer_id=401d68fb-fe97-4c56-84a6-5651cdbde747&tx_hash=0x6bebb2737f6a6f0fb23c6f5da2cec255404e4fb440034d6608697a8d41bed440", {
  method: "GET",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
  },
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.get(
    "https://api.gravv.xyz/v1/transactions?customer_id=401d68fb-fe97-4c56-84a6-5651cdbde747&tx_hash=0x6bebb2737f6a6f0fb23c6f5da2cec255404e4fb440034d6608697a8d41bed440",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

The row is `transaction_type` `crypto_funding`, `direction` `in`, `status` `completed`, with the wallet's id as `receiving_account_id`. If Gravv does not record the coin or network as a deposit, there is no row.

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| 400 `missing or invalid customer_id` | `customer_id` is missing | Send the customer's id |
| 400 `missing or invalid network` | `network` is missing | Send a network from the table |
| 400 `network must be one of the following values: …` | The network is unknown or not lower case, such as `Polygon` | Send a lower-case network from the table |
| 400 `Network not supported: tron` | Gravv refuses customer wallets on `tron` | Use a network from the table |
| 400 `invalid wallet_type "treasury": must be "customer" or "master"` | `wallet_type` has a wrong value | Leave it out. It defaults to `customer` |
| 400 `internal server error` | `customer_id` is not a UUID, or `name` is over 255 characters | Fix it and retry with a **new** key |
| 400 `service unavailable` | A short outage | Retry with a **new** key |
| 400 `missing idempotency key in request headers` | The `Idempotency-Key` header is missing | Add one |
| 422 `idempotency_key_reused` | You reused a key with a different body | Use a new key for a new wallet |
| 403 with `required_any` `["account:create"]` | The scoped key lacks the permission | Use a key whose role has `account:create` |
| Two wallets for one customer on one network | You made two calls with two different keys | Use one key per wallet you mean to create |
| A `COMPLETED` event but no row | Gravv does not record the coin or network as a deposit | Do not credit it. See [Find a missing stablecoin deposit](/recipes/wallets-and-deposits/find-a-missing-stablecoin-deposit) |
| The deposit never arrives | Wrong network, wrong address or a coin that is not supported | See [Find a missing stablecoin deposit](/recipes/wallets-and-deposits/find-a-missing-stablecoin-deposit) |

## Webhooks you will receive

- `inflow.onchain.completed` when Gravv sees the coins. Read `asset_symbol`, `amount`, `blockchain_network`, `destination.wallet_address` and `tx_hash`.
- `inflow.onchain.failed`, with `status` `FAILED` and `remark` `inbound transaction failed on chain`, when the sender's transaction fails on chain.

Gravv sends no webhook when it creates a wallet. See [On-chain deposit events](/platform/webhooks/event-catalogue#on-chain-deposits).

## Testing in sandbox

A sandbox key creates test-network wallets. On EVM sandbox wallets, the `USDC` balance is the **sandbox test dollar**, so USDC from a public faucet does not show there. Adding test dollars to a standalone EVM wallet through the API is Not yet available. To test money arriving, use a [simulated deposit](/recipes/accounts-v2/test-accounts-with-a-simulated-deposit) on an account, or use a faucet with a Stellar or Solana wallet. See [Fund wallets during testing](/platform/wallets/fund-wallets-during-testing).

## Reference

- [Create a wallet](/api-reference/wallets/post-v1-wallets)
- [Get a single wallet](/api-reference/wallets/get-v1-wallets-wallet-id)
- [List transactions](/api-reference/transactions/get-v1-transactions)
- [Create a wallet (guide)](/platform/wallets/create-a-wallet)
