Stablecoin Whale Monitor
Repository
Documentation

Documentation

API reference

Three read-only JSON endpoints that serve the same records as the monitor.

Overview

Base path
/api on the same origin as the website, https://swm.josephdara.com
Authentication
None. Read-only
Format
JSON, UTF-8
Rate limit
60 requests per minute per IP. Over the limit: 429 for 60 seconds
Caching
Cache-Control: no-store
GET

/api/transfers

Returns retained qualifying transfers, newest first, with optional filters and cursor pagination.

Query parameters

All parameters are optional and can be combined. An empty value is treated as absent.

NameTypeRules
tokenstringUSDC or USDT, any letter case.
minimumAmountdecimal stringDigits with up to 6 decimal places, no commas or exponent. Compared exactly in base units. Values below the indexing threshold return the same results as no minimum.
addressstring0x plus 40 hexadecimal characters, any letter case. Matches the sender or the receiver.
limitinteger1 to 50. Default 50.
cursorstringThe nextCursor from a previous response with the same filters. Up to 512 characters.

Examples

Newest 50 qualifying transfers:

curl
curl "https://swm.josephdara.com/api/transfers"

USDT transfers of at least 250,000, ten at a time:

curl
curl "https://swm.josephdara.com/api/transfers?token=USDT&minimumAmount=250000&limit=10"

Everything sent or received by one address:

curl
curl "https://swm.josephdara.com/api/transfers?address=0x818319478da6bd0c621de49f145fda9988c79fc3"

The next page, keeping the same filters:

curl
curl "https://swm.josephdara.com/api/transfers?token=USDT&minimumAmount=250000&limit=10&cursor=<nextCursor>"

Response

200 OKExample data
{
  "items": [
    {
      "chain": "ethereum",
      "chainId": 1,
      "token": "USDT",
      "tokenContract": "0xdac17f958d2ee523a2206206994597c13d831ec7",
      "transactionHash": "0xced9041dff02cee737443e210471948d33296c87009e8a7f770d9106fd287db7",
      "blockNumber": 26248680,
      "blockTimestamp": "2026-10-22T13:52:59.000Z",
      "logIndex": 257,
      "from": "0x818319478da6bd0c621de49f145fda9988c79fc3",
      "to": "0x5526f7eaed46725a2a7b860dcd6c8a1f8b46287c",
      "rawAmount": "2750000500000",
      "amount": "2750000.5",
      "threshold": "100000",
      "transactionStatus": "success",
      "transferType": "normal",
      "detectedAt": "2026-10-22T13:54:41.512Z"
    }
  ],
  "nextCursor": "eyJrZXkiOiIxOjB4Y2VkOTA0MWRmZjAyY2VlNzM3NDQzZTIxMDQ3MTk0OGQzMzI5NmM4NzAwOWU4YTdmNzcwZDkxMDZmZDI4N2RiNzoyNTcifQ",
  "total": 312,
  "counts": { "USDC": 0, "USDT": 312 },
  "refreshedAt": "2026-10-22T14:04:52.204Z"
}

Envelope

FieldTypeMeaning
itemsarrayTransfers on this page, newest first by block number, then log index.
nextCursorstring or nullPass as cursor for the next, older page. null on the last page.
totalintegerNumber of retained transfers that match the filters, across all pages.
countsobjectThe same filtered set split by token: USDC and USDT.
refreshedAtISO 8601 stringWhen the API last reloaded its index from storage.

Transfer object

FieldTypeMeaning
chain, chainIdstring, integerAlways "ethereum" and 1.
tokenstring"USDC" or "USDT".
tokenContractstringLowercase contract address that emitted the event.
from, tostringLowercase sender and receiver addresses.
amountdecimal stringExact token amount, up to 6 decimals, trailing zeros removed.
rawAmountinteger stringExact amount in base units.
thresholddecimal stringIndexing threshold when the transfer was recorded.
blockNumberintegerBlock containing the transfer.
blockTimestampISO 8601 stringBlock time in UTC.
logIndexintegerPosition of the event in its block.
transactionHashstringLowercase transaction hash.
transactionStatusstringAlways "success". Failed transactions emit no Transfer events.
transferTypestring"mint" when from is the zero address, "burn" when to is, otherwise "normal".
detectedAtISO 8601 stringWhen the indexer recorded the transfer.

A transfer is uniquely identified by chainId, transactionHash and logIndex. One transaction can appear in several items.

amount and rawAmount

Both fields carry the same exact value. amount is for people: "2750000.5". rawAmount is for arithmetic: "2750000500000", which is the amount multiplied by 1,000,000 (10 to the power of 6).

Both are strings so no precision is lost. Do the math on rawAmount with a big integer type. Converting amount to a floating point number can round large values.

JavaScript
const units = BigInt(item.rawAmount); // 2750000500000n
const whole = units / 1_000_000n;      // 2750000n

Amounts are token units. The API does not return prices or USD values.

Pagination and cursor expiry

Results are always newest first. To read older transfers, pass nextCursor back as cursor with the same filters. The cursor points at the last transfer of the page you received, and the next page starts right after it.

What total and counts mean

total and counts describe the retained transfers that match your filters at refreshedAt. They can fall between requests as old transfers are removed, and they ignore limit and cursor.

Errors

Errors from the API use one shape:

JSON
{ "error": "Invalid address filter" }
StatusMessageWhat to do
400Invalid token filter, Invalid minimumAmount filter, Invalid address filter, Invalid limit, limit must be between 1 and 50, Invalid cursorFix the parameter. See the rules above.
400Cursor is invalid or no longer availableThe page aged out. Request again without cursor.
429From the load balancer, may not be JSONMore than 60 requests in a minute from your IP. Wait 60 seconds, then slow down.
500Internal server errorDetails are hidden on purpose. Retry later.
503Event index could not be refreshedThe index has never loaded or is over 10 minutes old. Check /api/health and retry later.
GET

/api/transfers/{txHash}

Returns every retained qualifying transfer in one transaction.

Path parameter
txHash: 0x plus 64 hexadecimal characters, any letter case
Order
Newest first, the same order as the feed
curl
curl "https://swm.josephdara.com/api/transfers/0xced9041dff02cee737443e210471948d33296c87009e8a7f770d9106fd287db7"
200 OKExample data
{
  "transactionHash": "0xced9041dff02cee737443e210471948d33296c87009e8a7f770d9106fd287db7",
  "items": [
    {
      "chain": "ethereum",
      "chainId": 1,
      "token": "USDT",
      "tokenContract": "0xdac17f958d2ee523a2206206994597c13d831ec7",
      "transactionHash": "0xced9041dff02cee737443e210471948d33296c87009e8a7f770d9106fd287db7",
      "blockNumber": 26248680,
      "blockTimestamp": "2026-10-22T13:52:59.000Z",
      "logIndex": 257,
      "from": "0x818319478da6bd0c621de49f145fda9988c79fc3",
      "to": "0x5526f7eaed46725a2a7b860dcd6c8a1f8b46287c",
      "rawAmount": "2750000500000",
      "amount": "2750000.5",
      "threshold": "100000",
      "transactionStatus": "success",
      "transferType": "normal",
      "detectedAt": "2026-10-22T13:54:41.512Z"
    },
    { "...": "a second qualifying transfer, logIndex 240" }
  ],
  "total": 2,
  "refreshedAt": "2026-10-22T14:04:52.204Z"
}
StatusMessageMeaning
400Invalid transaction hashThe path is not a 32-byte hash.
404Transaction was not found in the indexed transfersNo matching transfers are currently indexed for this transaction.
503Event index could not be refreshedSame as for /api/transfers.
GET

/api/health

Reports index freshness and chain progress. The monitor and the status page read it every 30 seconds.

curl
curl "https://swm.josephdara.com/api/health"
200 OKExample data
{
  "status": "ok",
  "chainId": 1,
  "lastProcessedBlock": 26248735,
  "chainHeadBlock": 26248741,
  "checkpointUpdatedAt": "2026-10-22T14:04:29.118Z",
  "checkpointAgeSeconds": 41,
  "retentionDays": 7,
  "indexThreshold": "100000",
  "confirmationBuffer": 6,
  "pollIntervalSeconds": 30,
  "indexedEvents": 4812,
  "indexRefreshedAt": "2026-10-22T14:04:52.204Z",
  "indexError": null,
  "lastRefreshError": null,
  "checkpointError": null
}
FieldMeaning
status"ok", or "degraded" when any of the three error fields is set.
lastProcessedBlockThe last block the indexer finished and saved. null before the first checkpoint.
chainHeadBlockThe newest block the indexer had seen when it saved that checkpoint. The gap to lastProcessedBlock is normally the confirmation buffer. null until a checkpoint records it.
checkpointUpdatedAt, checkpointAgeSecondsWhen that checkpoint was written and how old it was when the API answered.
retentionDaysThe retention window the API applies.
indexThresholdThe minimum transfer amount the indexer keeps, as a decimal string in whole tokens.
confirmationBufferHow many of the newest blocks the indexer waits behind before reading a block.
pollIntervalSecondsThe pause between indexer cycles. indexThreshold, confirmationBuffer and pollIntervalSeconds are null when the API is not configured with them.
indexedEventsTransfers held in memory, inside the retention window.
indexRefreshedAtWhen the API last reloaded its index from storage.
indexErrorSet when the index has never loaded or is older than 10 minutes. The response is then HTTP 503.
lastRefreshErrorThe last refresh failed. The API keeps serving its last good index.
checkpointErrorThe API could not read the indexer checkpoint.

The endpoint answers 503 with the same JSON body when indexError is set, and 200 otherwise, even when status is "degraded".

Why HTTP 200 does not always mean healthy

The transfer endpoints answer 200 as long as the index refreshed within the last 10 minutes. A response can therefore be correct but several minutes old, and the indexer behind it can be stalled even while the API keeps serving.

Found a problem in this page? Open an issue in the repository.