Resolve a Stellar Asset Contract (SAC) address to the asset it wraps. Cloudflare Worker + D1.
Public instance: https://sac-resolver.lightsail.network/{mainnet|testnet}/{contract_id}
GET https://sac-resolver.lightsail.network/mainnet/CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75
→ 200 {"type":"credit_alphanum4","code":"USDC","issuer":"GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"}
GET https://sac-resolver.lightsail.network/mainnet/CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA
→ 200 {"type":"native"}
| Status | Meaning | Notes |
|---|---|---|
| 200 | Resolved | native / credit_alphanum4 / credit_alphanum12 |
404 not_a_stellar_asset_contract |
Contract exists but is not a SAC (wasm contract) | Permanent |
404 not_found |
The SAC has never been deployed on that network | Re-checked after 15 minutes |
| 400 | Malformed path or invalid StrKey | Never hits RPC |
| 503 | All RPC endpoints failed | Not cached, Retry-After: 10 |
Debug headers: x-sac-cache: hit|miss (edge cache), x-sac-source: db|rpc.
- Fetch the contract instance entry (
LedgerKeyContractInstance) withrpc.Server.getLedgerEntries. - Require the executable to be
StellarAsset, then read the asset from theAssetInfoinstance-storage entry. If that is missing, fall back toMETADATA.name(CODE:ISSUERornative). - Re-derive the SAC address for that network from the parsed asset and compare it with the requested address. A mismatch is treated as not a SAC, so a contract cannot lie about which asset it represents.
Every layer exists to avoid asking RPC twice for the same address. A SAC address is sha256(network, asset), so the asset behind an address can never change. Therefore:
| Result | D1 | Edge cache (Cache API) |
|---|---|---|
| ok / not_sac | Permanent, RPC is never queried again | s-maxage=86400 (CACHE_TTL_OK), browsers 1h |
| not_found | checked_at is stored; served as 404 without RPC for 15 minutes (NOT_FOUND_RECHECK_S) |
s-maxage=900, browsers 60s |
| All RPCs down | If a stale not_found row exists it is returned; otherwise 503 and nothing is written |
Not cached |
Request flow: edge cache → D1 → RPC. RPC endpoints are tried in order; the next one is only used on transport/RPC errors, never on "not found".
npm install
npx wrangler d1 create sac-resolver # put the printed database_id into the DB binding in wrangler.jsonc
# (wrangler appends a second binding; delete it and keep "DB")
npm run db:migrate:remote
npm run deployLocal development:
npm run db:migrate:local
npm run dev
curl localhost:8787/mainnet/CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMAVerify the parsing logic directly against RPC, without the Worker:
npm run probe -- mainnet CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75
npm run probe -- testnet # defaults to the testnet native XLM contractEverything lives in vars in wrangler.jsonc: RPC_URLS_MAINNET / RPC_URLS_TESTNET (comma separated, tried in order), RPC_TIMEOUT_MS, CACHE_TTL_OK. The not_found re-check interval is the constant NOT_FOUND_RECHECK_S in src/db.ts.
wrangler.jsonc contains no secrets: the D1 database_id is only usable together with an authenticated Cloudflare API token, and the RPC URLs are public. It is safe to commit. If you fork this, replace database_id with your own.