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
- Ada asks for a deposit address. You create a Polygon wallet for her with the address
0xc47B2D22812ee674Ee5182320FCCA83854f21962. - Ada withdraws 250 USDC from her exchange, picks the Polygon network and pastes that address.
- Gravv sees 250 USDC arrive, records a
crypto_fundingrow of 250 for Ada, and sends youinflow.onchain.completed. - 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 checkcustomer_idwhen 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_symbolwhen it is present, becauseassetcan be the token's contract address. blockchain_networkis 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_idhere is<network>:<hash>, not the id of a transaction row. Find the row withtx_hashin 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.completedwhen Gravv sees the coins. Readasset_symbol,amount,blockchain_network,destination.wallet_addressandtx_hash.inflow.onchain.failed, withstatusFAILEDandremarkinbound 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.