How to Track Token Velocity on Stellar for Stablecoins and RWAs
Token velocity measures how frequently an asset changes hands over a given period. It is one of the most revealing metrics for understanding whether a token is being actively used in commerce and settlement — or just sitting in wallets waiting for a price move. For stablecoins like USDC on Stellar and tokenized real-world assets (RWAs), velocity is a direct indicator of product-market fit.
What Token Velocity Is
Token velocity is calculated as:
Velocity = Transaction Volume / Average Supply in Circulation
If a stablecoin has 100 million tokens in circulation and 500 million in transaction volume over a month, the velocity is 5.0. That means each token changed hands an average of 5 times during the period.
| Velocity | What It Means |
|---|---|
| < 1.0 | Token is mostly held, low usage |
| 1.0 - 5.0 | Moderate usage, healthy for store-of-value assets |
| 5.0 - 20.0 | Active usage, typical for payment stablecoins |
| > 20.0 | Very high turnover, common for settlement tokens |
Why It Matters for Stablecoins
Stablecoins are designed to be spent, not held. A USDC token sitting in a wallet for months is not doing its job. High velocity means the stablecoin is being used for:
On Stellar, USDC has over 2.1 million holders. But how many of them are actively transacting? Velocity answers that question.
Why It Matters for RWAs
Real-world assets on Stellar — tokenized treasury bills, bonds, real estate tokens — have a different velocity profile. You expect lower velocity (these are investment instruments), but zero velocity is a red flag:
Fetching Payment Data from Horizon
To calculate velocity, you need two things: transaction volume and circulating supply. Start with payments:
const HORIZON = 'https://horizon.stellar.org';
async function getPaymentVolume(assetCode, assetIssuer, hours = 24) {
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
let totalVolume = 0;
let paymentCount = 0;
let url = `${HORIZON}/payments?order=desc&limit=200`;
while (url) {
const res = await fetch(url);
const data = await res.json();
const records = data._embedded.records;
let reachedCutoff = false;
for (const record of records) {
if (new Date(record.created_at) < cutoff) {
reachedCutoff = true;
break;
}
if (
record.type === 'payment' &&
record.asset_code === assetCode &&
record.asset_issuer === assetIssuer
) {
totalVolume += parseFloat(record.amount);
paymentCount++;
}
}
if (reachedCutoff || records.length < 200) break;
url = data._links?.next?.href;
}
return { totalVolume, paymentCount, periodHours: hours };
}Fetching Circulating Supply
For Stellar assets, the circulating supply is the total amount issued minus what the issuer holds:
async function getCirculatingSupply(assetCode, assetIssuer) {
const res = await fetch(
`${HORIZON}/assets?asset_code=${assetCode}&asset_issuer=${assetIssuer}`
);
const data = await res.json();
const asset = data._embedded.records[0];
if (!asset) return null;
const totalSupply = parseFloat(asset.amount);
const numAccounts = asset.num_accounts;
// Get issuer balance to calculate circulating supply
const issuerRes = await fetch(`${HORIZON}/accounts/${assetIssuer}`);
const issuer = await issuerRes.json();
const issuerBalance = issuer.balances.find(
b => b.asset_code === assetCode && b.asset_issuer === assetIssuer
);
const issuerHeld = issuerBalance ? parseFloat(issuerBalance.balance) : 0;
return {
totalSupply,
issuerHeld,
circulating: totalSupply - issuerHeld,
holders: numAccounts,
};
}Calculating Velocity
async function calculateVelocity(assetCode, assetIssuer, periodHours = 24) {
const [volume, supply] = await Promise.all([
getPaymentVolume(assetCode, assetIssuer, periodHours),
getCirculatingSupply(assetCode, assetIssuer),
]);
if (!supply || supply.circulating === 0) {
return { error: 'Cannot calculate — zero circulating supply' };
}
const annualizedVolume = volume.totalVolume * (8760 / periodHours);
const velocity = annualizedVolume / supply.circulating;
return {
asset: `${assetCode}:${assetIssuer.slice(0, 4)}...${assetIssuer.slice(-4)}`,
periodHours,
transactionVolume: volume.totalVolume,
paymentCount: volume.paymentCount,
circulatingSupply: supply.circulating,
holders: supply.holders,
annualizedVelocity: Math.round(velocity * 100) / 100,
};
}Tracking Velocity Over Time
To see trends, store daily snapshots and compare:
async function trackVelocityTrend(assetCode, assetIssuer, days = 7) {
const snapshots = [];
for (let i = 0; i < days; i++) {
const offsetHours = i * 24;
// In production, you would query historical data from your database
// This simplified version shows the concept
const velocity = await calculateVelocity(assetCode, assetIssuer, 24);
snapshots.push({
date: new Date(Date.now() - offsetHours * 60 * 60 * 1000).toISOString().split('T')[0],
velocity: velocity.annualizedVelocity,
volume: velocity.transactionVolume,
payments: velocity.paymentCount,
});
}
return snapshots;
}Interpreting Velocity for Specific Asset Types
USDC on Stellar
USDC is the most liquid stablecoin on Stellar. Expect high velocity driven by:
A sudden velocity drop could indicate a shift in remittance corridors or reduced DEX activity.
Tokenized Treasury Bills
Assets like Franklin Templeton's BENJI have low velocity by design. These are buy-and-hold instruments. Watch for:
Euro Stablecoins (EURC, EURAU)
Euro-denominated stablecoins on Stellar target European settlement. Velocity patterns follow business hours and payment cycles. Expect higher velocity on weekdays and lower on weekends.
Building a Velocity Dashboard
Combine the above functions into a monitoring dashboard:
async function buildVelocityDashboard() {
const assets = [
{ code: 'USDC', issuer: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN' },
{ code: 'yXLM', issuer: 'GARDNV3Q7YGT4MASTV2SUDBOMUQTTSKZYA2FKYWP45HJRYSALIDNGGEH' },
// Add more assets as needed
];
const results = await Promise.all(
assets.map(a => calculateVelocity(a.code, a.issuer, 24))
);
return results.sort((a, b) => b.annualizedVelocity - a.annualizedVelocity);
}Using LumenQuery Analytics
The LumenQuery Analytics dashboard tracks token velocity, whale movements, and issuer risk for all Stellar assets. It pre-computes velocity metrics with Redis caching so you do not need to paginate through thousands of payment records yourself.
The /api/analytics/tokens endpoint returns:
Alerts Based on Velocity Changes
Set up alerts for velocity anomalies:
function checkVelocityAlert(current, previous, threshold = 0.5) {
if (!previous) return null;
const change = (current - previous) / previous;
if (change > threshold) {
return { type: 'spike', change: `+${(change * 100).toFixed(0)}%`, message: 'Unusual velocity increase' };
}
if (change < -threshold) {
return { type: 'drop', change: `${(change * 100).toFixed(0)}%`, message: 'Significant velocity decrease' };
}
return null;
}A 50% spike in velocity for a stablecoin could mean a major remittance corridor opened up. A 50% drop could indicate a liquidity problem or a migration to a different asset.
Next Steps
*Track token velocity without building infrastructure from scratch. LumenQuery provides pre-computed analytics with caching, so you can focus on insights instead of pagination. Start free.*