Give a customer a stablecoin address on an EVM network

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} 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.

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.

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"
}
'
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());
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())
{
  "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.

{
  "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

curl --request GET \
 --url https://api.gravv.xyz/v1/wallets/bbab27f6-04b8-457d-83ed-b92009758340 \
 --header 'Api-Key: <API_KEY>'
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());
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())
{
  "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.

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>'
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());
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())

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
The deposit never arrives Wrong network, wrong address or a coin that is not supported See 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.

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 on an account, or use a faucet with a Stellar or Solana wallet. See Fund wallets during testing.

Reference

Was this page helpful?