코드에서 피드를 페이지 단위로 읽고 응답을 캐시하려면 어떻게 합니까?

전송과 거래 · 커서 루프, 조건부 GET, max-age를 폴링 간격으로 읽는 법 — 네 가지 언어로

  • API 가이드
  • 8분
  • 중급
  • 개발자
  • cursor
  • ETag
  • Cache-Control

ACHIVX Academy 원문 (영어) ↗

세 가지 습관이며, 각각 몇 줄이면 됩니다. 페이지 이동은 커서가 없는 페이지가 올 때까지 같은 쿼리를 cursor=<next_cursor>와 함께 반복하는 방식으로 합니다. 순회 도중에 필터를 바꾸면 안 됩니다. 재검증은 If-None-Match: <etag>로 합니다. 일치하는 응답은 본문 없는 304입니다(/v1/networks가 2026-10-24에 Age: 140과 함께 이렇게 응답했습니다). max-age 간격으로 폴링합니다. 피드는 15초, 동기화 상태는 5초, 판정은 60초(순회가 중단된 경우 10초), 레지스트리는 300초, 순위는 3,600초입니다. 더 자주 폴링해도 같은 바이트가 돌아옵니다. 여기의 모든 규칙은 스캐너가 보내는 헤더에서 나온 것이며, 코드가 그것을 보여 줍니다.

페이지 이동: next_cursor 따라가기

피드, 동결 기록 원장, 제안 목록은 최신순으로, 한 페이지씩 제공됩니다. 커서는 불투명한 값이며, 유일한 규칙은 정확히 같은 필터와 함께 되돌려 보내는 것입니다. 2026-10-24에 chain=1&limit=3의 두 번째 페이지에는 next_cursor와 prev_cursor가 모두 있었고, total은 첫 페이지에만 있었습니다.

curl

cursor=
while :; do
  page=$(curl -s "https://stablescan.achivx.com/v1/transfers?chain=1&recipient=$ADDR&limit=200${cursor:+&cursor=$cursor}")
  echo "$page" | jq -r '.items[] | "\(.block_time) \(.payer) \(.amount.amount) \(.amount.token)"'
  cursor=$(echo "$page" | jq -r '.next_cursor // empty')
  [ -z "$cursor" ] && break
  sleep 0.1
done

Python

def pages(path, **params):
    '''yield every page of a cursor-paged route; stop at the page without next_cursor'''
    cursor = None
    while True:
        page = scan(path, **params, **({"cursor": cursor} if cursor else {}))
        yield page
        cursor = page.get("next_cursor")
        if not cursor:
            return

received = [row for page in pages("/v1/transfers", chain=1, recipient=addr, limit=200) for row in page["items"]]

Go

func allTransfers(q url.Values) ([]transfer, error) {
	var out []transfer
	for {
		var page struct {
			Items      []transfer `json:"items"`
			NextCursor string     `json:"next_cursor"`
		}
		if err := get("/v1/transfers", q, &page); err != nil {
			return out, err
		}
		out = append(out, page.Items...)
		if page.NextCursor == "" {
			return out, nil
		}
		q.Set("cursor", page.NextCursor)
	}
}

TypeScript

async function* pages<T extends { next_cursor?: string }>(path: string, params: Params) {
  let cursor: string | undefined;
  do {
    const page = await scan<T>(path, { ...params, cursor });
    yield page;
    cursor = page.next_cursor;
  } while (cursor);
}
for await (const page of pages<Page>("/v1/transfers", { chain: 1, recipient: addr, limit: 200 })) process(page.items);

멀리 순회하기 전에 알아 둘 점이 두 가지 있습니다. hot_window_start가 하한입니다. 그보다 오래된 행은 보관 처리되며, 순회는 기록의 시작이 아니라 그 지점에서 끝납니다. 또한 첫 페이지의 total은 25,000에서 상한이 걸립니다("capped": true). 순회는 개수가 말하는 것보다 길 수 있습니다. 저장해 둔 커서에서 dir=newer로 순회하는 것이 폴러가 따라잡는 방법입니다. 멈춘 지점부터 앞으로 페이지를 넘깁니다.

재검증: If-None-Match

모든 GET 응답에는 ETag가 있습니다(Access-Control-Expose-Headers: ETag도 있으므로 브라우저 클라이언트도 읽을 수 있습니다). 이를 되돌려 보내면 일치하는 응답은 본문 없는 304 Not Modified입니다.

curl -s -D - -o /dev/null -H 'If-None-Match: "8fc4bafb57ec938fe40dc8793281336e"' https://stablescan.achivx.com/v1/networks
HTTP/2 304
etag: "8fc4bafb57ec938fe40dc8793281336e"
cache-control: public, max-age=300, stale-while-revalidate=60, stale-if-error=3600

태그는 본문에서 만들어지므로, 본문이 같을 때 일치합니다. 레지스트리, /v1/sanctions, /v1/methodology, 재생된 판정이 그렇습니다(제재 대상 예시의 위험 응답은 두 번째 조회에서 같은 etag와 Age: 2로 응답했습니다. 오래된 태그로 한 첫 조회는 computed_at이 바뀌었기 때문에 200으로 응답했습니다). freshness_seconds가 매초 바뀌는 주소 개요는 거의 일치하지 않습니다. 엣지가 약한 태그(W/"…", Markdown 페이지에서 확인됨)를 줄 수도 있으며, 이것도 일치합니다.

Python

import urllib.error

def scan_cached(path, cache, **params):
    '''cache: {url: (etag, body)}; a 304 returns the cached body'''
    url = BASE + path + ("?" + urllib.parse.urlencode(params) if params else "")
    headers = {"Accept": "application/json"}
    if url in cache:
        headers["If-None-Match"] = cache[url][0]
    req = urllib.request.Request(url, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            body = json.load(r); cache[url] = (r.headers.get("ETag"), body); return body
    except urllib.error.HTTPError as e:
        if e.code == 304:
            return cache[url][1]
        raise

Go

// etags caches the last body per URL; a 304 returns it.
var etags = map[string]struct {
	tag  string
	body []byte
}{}

func getCached(u string) ([]byte, error) {
	req, _ := http.NewRequest(http.MethodGet, u, nil)
	if c, ok := etags[u]; ok {
		req.Header.Set("If-None-Match", c.tag)
	}
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode == http.StatusNotModified {
		return etags[u].body, nil
	}
	body, err := io.ReadAll(res.Body)
	if err == nil && res.StatusCode == 200 {
		etags[u] = struct {
			tag  string
			body []byte
		}{res.Header.Get("ETag"), body}
	}
	return body, err
}

TypeScript

const etags = new Map<string, { tag: string; body: unknown }>();
async function scanCached<T>(url: string): Promise<T> {
  const hit = etags.get(url);
  const res = await fetch(url, { headers: hit ? { "if-none-match": hit.tag } : {} });
  if (res.status === 304 && hit) return hit.body as T;
  const body = (await res.json()) as T;
  const tag = res.headers.get("etag");
  if (res.ok && tag) etags.set(url, { tag, body });
  return body;
}

폴링: Cache-Control을 간격으로 읽기

경로2026-10-24의 max-age폴링 간격
/v1/sync/status55초
/v1/transfers, /v1/verify1515초
/v1/freezes/acts, /v1/freezes/proposals3030초
/v1/addresses/{a}/balances3030초
주소 개요, /risk, /exposure, /graph, /walk, /volume, /approvals, /heuristics, /v1/stats/home, /v1/stats/overview, /v1/freezes/networks, /v1/search601분(/risk와 /exposure: 순회가 중단된 경우 10)
/v1/sanctions, /v1/subscriptions, /v1/freezes/guide, /v1/tokens/{t}/freezes3005분
/v1/networks, /v1/tokens(stale-while-revalidate=60, stale-if-error=3600 포함)3005분
/v1/entities…60010분 — 어차피 하루에 한 번 생성됨(built_at)
/v1/freezes?tx=, /v1/tokens/{a}/contract, /v1/tokens/{t}/routes, /addresses36001시간
/v1/stats/slo, /health, /ready, 모든 오류, 일부가 빠진 모든 응답no-store—

max-age보다 자주 폴링한 판정은 같은 computed_at을 돌려줍니다. 15초보다 자주 폴링한 피드는 같은 페이지를 돌려줍니다. 재생된 응답의 Age 헤더는 사본이 얼마나 오래되었는지 알려 줍니다. 200에 붙은 no-store가 무엇을 요구하는지(잠시 기다린 뒤 다시 요청)는 응답 읽기에서 다룹니다.

Stablecoin scanner ACHIVX에서

출처

더 알아보기

전송과 거래 · cursor · ETag · Cache-Control · 2026-10-24 작성, openapi.json, agents.md 및 당일 실제 호출 기반. 구조는 인용한 것이며, 수치는 날짜 기준이고 인덱스에 따라 바뀝니다.

이 페이지는 ACHIVX Academy 영어 원문을 한국어로 옮긴 것입니다. 번역과 원문이 다르면 영어 원문이 기준입니다.