.md

Get every FT transfer for a Stacks address

Fungible-token transfers are decoded and indexed, so an address's history is a filtered read. A transfer has a sender and a recipient, so you read each side with its own call. One key, one call; here is the received side:

$curl "$SECONDLAYER_API_URL/v1/index/ft-transfers?recipient=SP3F3GBGAR8781BF831JT4W7J5DR12ZB15A575DEC&limit=1" \
-H "Authorization: Bearer sk-sl_…"
200response.jsonjson
{
"events": [
{
"cursor": "9331956:4",
"block_height": 9331956,
"block_time": "2026-10-27T04:25:47.000Z",
"tx_id": "0xf230dcb1f4550142d72e6b07e5f2769c8e533c857d26b1c7dac9a62d6c252804",
"tx_index": 2,
"event_index": 4,
"event_type": "ft_transfer",
"contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
"asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
"sender": "SM1793C4R5PZ4NS4VQ4WMP7SKKYVH8JZEWSZ9HCCR.xyk-pool-sbtc-stx-v-1-1",
"recipient": "SP3F3GBGAR8781BF831JT4W7J5DR12ZB15A575DEC",
"amount": "152"
}
],
"next_cursor": "9331956:4",
"tip": { "block_height": 9348864, "finalized_height": 9348069, "lag_seconds": 23 },
"reorgs": []
}

There is no single address filter: each side of a transfer is filtered on its own, so make one call per side.

const [received, sent] = await Promise.all([
index.ftTransfers.list({ recipient: address }),
index.ftTransfers.list({ sender: address }),
]);
  • Merge the two lists by block_height.
  • A transfer to yourself is in both, so drop repeats by cursor.

A read with no fromHeight or cursor covers only the most recent ~17,280 blocks, so pass a start to go further back.

Narrow byParam
One tokencontractId or assetIdentifier
A block rangefromHeight, toHeight
Newest firstorder: "desc"

amount is a string, in the token's smallest unit.

Pass the last next_cursor back as the next cursor. It comes back null only on an empty page, which ends the loop.

import type { FtTransfersEnvelope } from "@secondlayer/sdk";
let cursor: string | null = null;
do {
const page: FtTransfersEnvelope = await index.ftTransfers.list({
recipient: address,
fromHeight: cursor ? undefined : 0,
cursor,
limit: 1000,
});
for (const transfer of page.events) await save(transfer);
cursor = page.next_cursor;
} while (cursor);

Pass a start height or a cursor, never both: drop the height once you hold a cursor. A cursor only resumes under the order that issued it. For a one-off export, walk() does the paging for you; it keeps no checkpoint (list, walk, consume).

Hosted reads are metered, with a free monthly allowance (What it costs). Request fields to trim each row.

Every filter and the response schema: List FT transfers. The decoded feeds are described in Index.

Edit on GitHub