API reference
Three read-only JSON endpoints that serve the same records as the monitor.
Overview
- Base path
/apion 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
/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.
| Name | Type | Rules |
|---|---|---|
token | string | USDC or USDT, any letter case. |
minimumAmount | decimal string | Digits 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. |
address | string | 0x plus 40 hexadecimal characters, any letter case. Matches the sender or the receiver. |
limit | integer | 1 to 50. Default 50. |
cursor | string | The nextCursor from a previous response with the same filters. Up to 512 characters. |
Examples
Newest 50 qualifying transfers:
curl "https://swm.josephdara.com/api/transfers"
USDT transfers of at least 250,000, ten at a time:
curl "https://swm.josephdara.com/api/transfers?token=USDT&minimumAmount=250000&limit=10"
Everything sent or received by one address:
curl "https://swm.josephdara.com/api/transfers?address=0x818319478da6bd0c621de49f145fda9988c79fc3"
The next page, keeping the same filters:
curl "https://swm.josephdara.com/api/transfers?token=USDT&minimumAmount=250000&limit=10&cursor=<nextCursor>"
Response
{
"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
| Field | Type | Meaning |
|---|---|---|
items | array | Transfers on this page, newest first by block number, then log index. |
nextCursor | string or null | Pass as cursor for the next, older page. null on the last page. |
total | integer | Number of retained transfers that match the filters, across all pages. |
counts | object | The same filtered set split by token: USDC and USDT. |
refreshedAt | ISO 8601 string | When the API last reloaded its index from storage. |
Transfer object
| Field | Type | Meaning |
|---|---|---|
chain, chainId | string, integer | Always "ethereum" and 1. |
token | string | "USDC" or "USDT". |
tokenContract | string | Lowercase contract address that emitted the event. |
from, to | string | Lowercase sender and receiver addresses. |
amount | decimal string | Exact token amount, up to 6 decimals, trailing zeros removed. |
rawAmount | integer string | Exact amount in base units. |
threshold | decimal string | Indexing threshold when the transfer was recorded. |
blockNumber | integer | Block containing the transfer. |
blockTimestamp | ISO 8601 string | Block time in UTC. |
logIndex | integer | Position of the event in its block. |
transactionHash | string | Lowercase transaction hash. |
transactionStatus | string | Always "success". Failed transactions emit no Transfer events. |
transferType | string | "mint" when from is the zero address, "burn" when to is, otherwise "normal". |
detectedAt | ISO 8601 string | When 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.
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.
- New transfers arriving at the top do not shift later pages, because each page is anchored to a transfer rather than an offset.
- There is no previous cursor. To go back, keep the cursors you used and reuse them.
- When the anchor transfer ages out of the 7-day window, the cursor stops working and the API returns 400
Cursor is invalid or no longer available. Start again without a cursor. - A cursor that cannot be decoded returns 400
Malformed cursor.
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:
{ "error": "Invalid address filter" }| Status | Message | What to do |
|---|---|---|
| 400 | Invalid token filter, Invalid minimumAmount filter, Invalid address filter, Invalid limit, limit must be between 1 and 50, Invalid cursor | Fix the parameter. See the rules above. |
| 400 | Cursor is invalid or no longer available | The page aged out. Request again without cursor. |
| 429 | From the load balancer, may not be JSON | More than 60 requests in a minute from your IP. Wait 60 seconds, then slow down. |
| 500 | Internal server error | Details are hidden on purpose. Retry later. |
| 503 | Event index could not be refreshed | The index has never loaded or is over 10 minutes old. Check /api/health and retry later. |
/api/transfers/{txHash}
Returns every retained qualifying transfer in one transaction.
- Path parameter
txHash:0xplus 64 hexadecimal characters, any letter case- Order
- Newest first, the same order as the feed
curl "https://swm.josephdara.com/api/transfers/0xced9041dff02cee737443e210471948d33296c87009e8a7f770d9106fd287db7"
{
"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"
}| Status | Message | Meaning |
|---|---|---|
| 400 | Invalid transaction hash | The path is not a 32-byte hash. |
| 404 | Transaction was not found in the indexed transfers | No matching transfers are currently indexed for this transaction. |
| 503 | Event index could not be refreshed | Same as for /api/transfers. |
/api/health
Reports index freshness and chain progress. The monitor and the status page read it every 30 seconds.
curl "https://swm.josephdara.com/api/health"
{
"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
}| Field | Meaning |
|---|---|
status | "ok", or "degraded" when any of the three error fields is set. |
lastProcessedBlock | The last block the indexer finished and saved. null before the first checkpoint. |
chainHeadBlock | The 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, checkpointAgeSeconds | When that checkpoint was written and how old it was when the API answered. |
retentionDays | The retention window the API applies. |
indexThreshold | The minimum transfer amount the indexer keeps, as a decimal string in whole tokens. |
confirmationBuffer | How many of the newest blocks the indexer waits behind before reading a block. |
pollIntervalSeconds | The pause between indexer cycles. indexThreshold, confirmationBuffer and pollIntervalSeconds are null when the API is not configured with them. |
indexedEvents | Transfers held in memory, inside the retention window. |
indexRefreshedAt | When the API last reloaded its index from storage. |
indexError | Set when the index has never loaded or is older than 10 minutes. The response is then HTTP 503. |
lastRefreshError | The last refresh failed. The API keeps serving its last good index. |
checkpointError | The 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.
- Compare
refreshedAtwith the current time to see how old the index is. - Call
GET /api/health. It can answer 200 with"status": "degraded"when a refresh or checkpoint read failed, and itscheckpointAgeSecondsshows whether the indexer is still moving. - Treat the data as current only when the status is
"ok"and both ages are small. The status page applies these rules for you.
Found a problem in this page? Open an issue in the repository.