레시피: 결제가 도착했고 동결되지 않았는지 확인하기
레시피 · 해시 하나를 찾아지고 확정될 때까지 네트워크의 최신성 범위 안에서 폴링한 뒤, 수신자의 동결 상태 확인
- API 가이드
- 9분
- 고급
- 개발자
- verify → sync → overview
결제는 /v1/verify?tx=가 found: true로 응답하고 해당 행의 finality가 final일 때 확인된 것입니다. 그때까지 프로그램은 네트워크의 freshness_seconds가 허용하는 동안 15초마다(이 경로의 max-age) 폴링하며, found: null을 아니요가 아니라 다시 요청으로 취급합니다. 찾은 뒤에는 수신자의 개요를 한 번 조회하면, 받은 스테이블코인이 그 체인에서 동결되었는지 알 수 있습니다 — 결제가 도착하고도 잃을 수 있는 경우입니다. 프로그램은 confirmed, pending, not-found, frozen 중 하나를, 각각 본 내용과 함께 출력합니다.
상태
/v1/verify의 응답 | 프로그램의 동작 |
|---|---|
found: true, 모든 전송이 finality: "final" | 수신자의 개요를 조회하고 confirmed 또는 frozen 출력 |
found: true, 어떤 전송이 finality: "pending" | 15초 기다린 뒤 다시 요청 — 행은 인덱싱되었지만 블록이 아직 확정되지 않았습니다 |
found: false | 각 네트워크의 동기화 시점 기준으로 어떤 핫 윈도우에도 없습니다. 해시가 네트워크의 인덱싱된 헤드보다 새로울 수 있는 동안(freshness_seconds) 기다린 뒤 not-found 출력 |
found: null | 어떤 네트워크가 응답하지 않았습니다. 기다렸다가 다시 요청 — 부정 결과가 아닙니다 |
해시가 hot_window_start보다 오래됨 | API가 볼 수 없습니다. 그 설명과 함께 not-found 출력 |
curl
#!/bin/sh
# confirm.sh <tx-hash> → confirmed | pending | not-found | frozen
B=https://stablescan.achivx.com; TX=$1; tries=0
while [ $tries -lt 40 ]; do # 40 × 15 s = ten minutes
v=$(curl -sf "$B/v1/verify?tx=$TX") || { sleep 15; tries=$((tries+1)); continue; }
found=$(echo "$v" | jq -r .found)
if [ "$found" = true ]; then
pending=$(echo "$v" | jq '[.transfers[] | select(.finality != "final")] | length')
[ "$pending" = 0 ] || { sleep 15; tries=$((tries+1)); continue; }
chain=$(echo "$v" | jq -r .chain_id); to=$(echo "$v" | jq -r '.transfers[0].recipient'); token=$(echo "$v" | jq -r '.transfers[0].amount.token')
status=$(curl -sf "$B/v1/addresses/$to?chain=$chain" | jq -r --arg t "$token" '.freeze[] | select(.token == $t) | .status')
case "$status" in frozen|seized) echo "frozen ($token on chain $chain)"; exit 3 ;; *) echo confirmed; exit 0 ;; esac
fi
sleep 15; tries=$((tries+1)) # false or null: the hash may be newer than a head
done
echo not-found; exit 1Python
import json, sys, time, urllib.parse, urllib.request
BASE = "https://stablescan.achivx.com"
def scan(path, **params):
req = urllib.request.Request(BASE + path + "?" + urllib.parse.urlencode(params), headers={"Accept": "application/json", "User-Agent": "confirm/1.0"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
def confirm(tx, max_wait=600):
'''returns ("confirmed"|"pending"|"not-found"|"frozen", detail)'''
deadline = time.time() + max_wait
while time.time() < deadline:
v = scan("/v1/verify", tx=tx)
if v["found"] is True:
rows = v["transfers"]
if rows and all(r["finality"] == "final" for r in rows):
to, token, chain = rows[0]["recipient"], rows[0]["amount"]["token"], v["chain_id"]
facts = scan(f"/v1/addresses/{to}", chain=chain)
pair = next((r for r in facts["freeze"] if r["token"] == token), None)
if pair and pair["status"] in ("frozen", "seized"):
return "frozen", {"chain": v["chain_slug"], "token": token, "recipient": to, "since": pair.get("since_time")}
return "confirmed", {"chain": v["chain_slug"], "block": rows[0]["block_number"], "block_time": rows[0]["block_time"],
"amount": int(rows[0]["amount"]["amount"]) / 10 ** rows[0]["amount"]["decimals"], "token": token,
"freeze_state": pair["status"] if pair else "not freezable"}
elif v["freeze"] and v["freeze"]["items"] and not rows:
return "confirmed", {"note": "the hash is an issuer act, not a transfer", "acts": v["freeze"]["items"]}
elif v["found"] is False:
# a negative over every hot window as of each network's sync — but the hash may be newer than a head
max_lag = max((c["sync"].get("freshness_seconds") or 0) for c in v["chains"])
if time.time() + max_lag > deadline:
return "not-found", {"as_of": v["as_of"], "hot_window_start": v["hot_window_start"], "max_freshness_seconds": max_lag}
time.sleep(15) # found: null, pending, or false-but-recent: ask again at max-age
return "pending", {"note": "still not final after %d s" % max_wait}
if __name__ == "__main__":
state, detail = confirm(sys.argv[1])
print(state); print(json.dumps(detail, indent=1), file=sys.stderr)Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"time"
)
const base = "https://stablescan.achivx.com"
type transfer struct {
Recipient string `json:"recipient"`
BlockNumber int64 `json:"block_number"`
Finality string `json:"finality"`
Amount struct {
Amount string `json:"amount"`
Decimals int `json:"decimals"`
Token string `json:"token"`
} `json:"amount"`
}
type verify struct {
Found *bool `json:"found"`
ChainID *int64 `json:"chain_id"`
ChainSlug string `json:"chain_slug"`
Transfers []transfer `json:"transfers"`
Chains []struct {
Sync struct {
FreshnessSeconds *int `json:"freshness_seconds"`
} `json:"sync"`
} `json:"chains"`
}
func get(path string, q url.Values, out any) error {
res, err := http.Get(base + path + "?" + q.Encode())
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
return fmt.Errorf("%s: HTTP %d", path, res.StatusCode)
}
return json.NewDecoder(res.Body).Decode(out)
}
func confirm(tx string, maxWait time.Duration) (string, string) {
deadline := time.Now().Add(maxWait)
for time.Now().Before(deadline) {
var v verify
if err := get("/v1/verify", url.Values{"tx": {tx}}, &v); err == nil && v.Found != nil {
if *v.Found {
final := len(v.Transfers) > 0
for _, t := range v.Transfers {
final = final && t.Finality == "final"
}
if final {
t := v.Transfers[0]
var facts struct {
Freeze []struct {
Token, Status string
} `json:"freeze"`
}
_ = get("/v1/addresses/"+t.Recipient, url.Values{"chain": {fmt.Sprint(*v.ChainID)}}, &facts)
for _, p := range facts.Freeze {
if p.Token == t.Amount.Token && (p.Status == "frozen" || p.Status == "seized") {
return "frozen", fmt.Sprintf("%s %s on %s", t.Recipient, p.Status, v.ChainSlug)
}
}
return "confirmed", fmt.Sprintf("%s %s in block %d on %s", t.Amount.Amount, t.Amount.Token, t.BlockNumber, v.ChainSlug)
}
} else {
maxLag := 0
for _, c := range v.Chains {
if c.Sync.FreshnessSeconds != nil && *c.Sync.FreshnessSeconds > maxLag {
maxLag = *c.Sync.FreshnessSeconds
}
}
if time.Now().Add(time.Duration(maxLag) * time.Second).After(deadline) {
return "not-found", "not in any hot window; the newest head is " + fmt.Sprint(maxLag) + " s behind"
}
}
}
time.Sleep(15 * time.Second) // null, pending or recent false: ask again at the route's max-age
}
return "pending", "not final within " + maxWait.String()
}
func main() {
state, detail := confirm(os.Args[1], 10*time.Minute)
fmt.Println(state)
fmt.Fprintln(os.Stderr, detail)
}TypeScript
const BASE = "https://stablescan.achivx.com";
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
type Transfer = { recipient: string; block_number: number; block_time: string; finality: "final" | "pending"; amount: { amount: string; decimals: number; token: string } };
type Verify = { found: boolean | null; chain_id: number | null; chain_slug?: string; transfers: Transfer[]; freeze: { items: unknown[] } | null; chains: { sync: { freshness_seconds: number | null } }[]; as_of: string; hot_window_start: string };
type Overview = { freeze: { token: string; status: string; since_time?: string }[] };
async function scan<T>(path: string, params: Record<string, string>): Promise<T> {
const res = await fetch(`${BASE}${path}?${new URLSearchParams(params)}`, { headers: { accept: "application/json" } });
if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
return (await res.json()) as T;
}
export async function confirm(tx: string, maxWaitMs = 600_000): Promise<[string, Record<string, unknown>]> {
const deadline = Date.now() + maxWaitMs;
while (Date.now() < deadline) {
const v = await scan<Verify>("/v1/verify", { tx });
if (v.found === true) {
const rows = v.transfers;
if (rows.length && rows.every((r) => r.finality === "final")) {
const [t] = rows;
const facts = await scan<Overview>(`/v1/addresses/${t.recipient}`, { chain: String(v.chain_id) });
const pair = facts.freeze.find((r) => r.token === t.amount.token);
if (pair && (pair.status === "frozen" || pair.status === "seized")) return ["frozen", { chain: v.chain_slug, token: t.amount.token, recipient: t.recipient, since: pair.since_time }];
return ["confirmed", { chain: v.chain_slug, block: t.block_number, block_time: t.block_time, amount: Number(BigInt(t.amount.amount)) / 10 ** t.amount.decimals, token: t.amount.token }];
}
if (!rows.length && v.freeze?.items.length) return ["confirmed", { note: "the hash is an issuer act, not a transfer", acts: v.freeze.items }];
} else if (v.found === false) {
const maxLag = Math.max(0, ...v.chains.map((c) => c.sync.freshness_seconds ?? 0));
if (Date.now() + maxLag * 1000 > deadline) return ["not-found", { as_of: v.as_of, hot_window_start: v.hot_window_start, max_freshness_seconds: maxLag }];
}
await sleep(15_000); // null, pending, or a false that may still be newer than a head
}
return ["pending", { note: `not final within ${maxWaitMs / 1000} s` }];
}
const [state, detail] = await confirm(process.argv[2]);
console.log(state); console.error(JSON.stringify(detail, null, 1));예시에 대해 실행하기
2026-10-24에 confirm 0x2b3252e52889309dcf91d8b24034d28a01d247e6229189e05e16409a1eb12ac3은 이더리움(Ethereum) 블록 26261992에서 USDC 결제를 10:49 UTC에 finality: "pending" 상태로 찾았습니다 — 블록은 네트워크의 확정된 헤드보다 73 뒤에 있었습니다. 프로그램은 행이 final이 될 때까지 폴링을 계속한 뒤, 수신자의 개요(USDC: clear)를 조회하고 confirmed를 출력했습니다. 0으로 이루어진 해시는 최신성 범위가 지난 뒤 not-found를 출력했습니다. 전날 한 주소를 동결한 기록인 0x00c3…1410은 발행사 기록이라는 설명과 함께 confirmed를 출력했습니다. 거래이긴 하지만 결제는 아닙니다.
프로그램이 알려 줄 수 없는 것
hot_window_start보다 오래된 전송(그날 아침 EVM 네트워크에서는 약 이틀)은 보관 처리되어 있습니다. 그 경우found: false는 일어나지 않음이 아니라 보이지 않음입니다. 과거 기록은 사이트의 거래 페이지를 여십시오.- 솔라나(Solana) 전송이 동결되었는지 여부: 솔라나 발행사 기록은 수집되지 않으며, 개요의 솔라나 쌍은
unknown으로 응답합니다. - 결제가 이 수신자에게 의도된 것이었는지 여부. 스캐너는 체인을 볼 뿐, 청구서를 보지 않습니다.
Stablecoin scanner ACHIVX에서
- 레시피를 실행한 결제: /v1/verify?tx=0x2b32…2ac3. 네트워크의 최신성: /v1/sync/status?chain=1.
- 같은 거래를 페이지로: /ethereum/tx/0x2b32…2ac3.
출처
더 알아보기
관련 질문
- 해시로 거래를 찾으려면 어떻게 합니까: /v1/verify 또는 /v1/transfers?tx=?
- 지금 네트워크의 데이터는 얼마나 최신입니까?
- 주소가 제재 대상인지, 동결되었는지, 알려진 서비스인지 어떻게 확인합니까?
레시피 · verify → sync → overview · 2026-10-24 작성, openapi.json, agents.md 및 당일 실제 호출 기반. 구조는 인용한 것이며, 수치는 날짜 기준이고 인덱스에 따라 바뀝니다.
이 페이지는 ACHIVX Academy 영어 원문을 한국어로 옮긴 것입니다. 번역과 원문이 다르면 영어 원문이 기준입니다.