코드에서 피드를 페이지 단위로 읽고 응답을 캐시하려면 어떻게 합니까?
전송과 거래 · 커서 루프, 조건부 GET, max-age를 폴링 간격으로 읽는 법 — 네 가지 언어로
- API 가이드
- 8분
- 중급
- 개발자
- cursor
- ETag
- Cache-Control
세 가지 습관이며, 각각 몇 줄이면 됩니다. 페이지 이동은 커서가 없는 페이지가 올 때까지 같은 쿼리를 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
donePython
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]
raiseGo
// 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/status | 5 | 5초 |
/v1/transfers, /v1/verify | 15 | 15초 |
/v1/freezes/acts, /v1/freezes/proposals | 30 | 30초 |
/v1/addresses/{a}/balances | 30 | 30초 |
주소 개요, /risk, /exposure, /graph, /walk, /volume, /approvals, /heuristics, /v1/stats/home, /v1/stats/overview, /v1/freezes/networks, /v1/search | 60 | 1분(/risk와 /exposure: 순회가 중단된 경우 10) |
/v1/sanctions, /v1/subscriptions, /v1/freezes/guide, /v1/tokens/{t}/freezes | 300 | 5분 |
/v1/networks, /v1/tokens(stale-while-revalidate=60, stale-if-error=3600 포함) | 300 | 5분 |
/v1/entities… | 600 | 10분 — 어차피 하루에 한 번 생성됨(built_at) |
/v1/freezes?tx=, /v1/tokens/{a}/contract, /v1/tokens/{t}/routes, /addresses | 3600 | 1시간 |
/v1/stats/slo, /health, /ready, 모든 오류, 일부가 빠진 모든 응답 | no-store | — |
max-age보다 자주 폴링한 판정은 같은 computed_at을 돌려줍니다. 15초보다 자주 폴링한 피드는 같은 페이지를 돌려줍니다. 재생된 응답의 Age 헤더는 사본이 얼마나 오래되었는지 알려 줍니다. 200에 붙은 no-store가 무엇을 요구하는지(잠시 기다린 뒤 다시 요청)는 응답 읽기에서 다룹니다.
Stablecoin scanner ACHIVX에서
next_cursor,total,hot_window_start가 있는 페이지: /v1/transfers?chain=1&limit=5.- ETag로 재검증되는 응답: /v1/networks. 매뉴얼의 규칙: /agents.md §6.
출처
- 에이전트 매뉴얼 — agents.md(버전 2026-10-05)
- REST 인터페이스의 OpenAPI 3.1 명세(API 1.4.0)
- 2026-10-24에 읽은 응답 헤더 — ETag가 붙은 /v1/networks와 304
더 알아보기
관련 질문
전송과 거래 · cursor · ETag · Cache-Control · 2026-10-24 작성, openapi.json, agents.md 및 당일 실제 호출 기반. 구조는 인용한 것이며, 수치는 날짜 기준이고 인덱스에 따라 바뀝니다.
이 페이지는 ACHIVX Academy 영어 원문을 한국어로 옮긴 것입니다. 번역과 원문이 다르면 영어 원문이 기준입니다.