Comparing ens with pyth
ens
View full →Author
@0xinit
Stars
53
Repository
0xinit/cryptoskills
ENS (Ethereum Name Service)
ENS maps human-readable names (alice.eth) to Ethereum addresses, content hashes, and arbitrary metadata. It is the identity layer for Ethereum — used for wallets, dApps, and onchain profiles. The architecture separates the registry (who owns a name) from resolvers (what data a name points to).
What You Probably Got Wrong
- ENS uses
namehash, not plain strings -- The registry and resolvers never see "alice.eth" as a string. Names are normalized (UTS-46), then hashed with the recursivenamehashalgorithm (EIP-137). If you pass a raw string to a contract call, it will not work. viem handles this automatically in its ENS actions but you must usenamehash()andlabelhash()for direct contract calls. - Registry vs Resolver vs Registrar -- three different contracts -- The Registry tracks name ownership and which resolver to use. The Resolver stores records (address, text, contenthash). The Registrar handles
.ethname registration and renewal. Confusing these is the most common ENS integration bug. .ethregistrar uses commit-reveal, not a single transaction -- Registration requires two transactions separated by at least 60 seconds: firstcommit(secret), wait, thenregister(name, owner, duration, secret, ...). This prevents frontrunning. Skipping the wait or reusing a secret will revert.- Reverse resolution is opt-in -- An address only has a "primary name" if the owner explicitly set it via the Reverse Registrar. Do not assume every address has a reverse record. Always handle
nullreturns fromgetEnsName(). - Name Wrapper changes ownership semantics -- Since 2023, ENS names can be "wrapped" as ERC-1155 tokens via the Name Wrapper contract. Wrapped names have fuses that permanently restrict operations (cannot unwrap, cannot set resolver, etc.). Check
isWrappedbefore assuming standard ownership patterns. - CCIP-Read (ERC-3668) enables offchain resolution -- Resolvers can return an
OffchainLookuperror that instructs the client to fetch data from an offchain gateway and verify it onchain. This powers offchain subdomains, L2 resolution, and gasless record updates. viem handles CCIP-Read automatically. - Wildcard resolution (ENSIP-10) is real -- Resolvers can implement
resolve(bytes name, bytes data)to handle any subdomain dynamically, even ones not explicitly registered. This is how services like cb.id and lens.xyz work. - ENS names expire --
.ethnames require annual renewal. Expired names enter a 90-day grace period, then a 21-day premium auction, then become available. Do not cache resolution results indefinitely. normalize()before any ENS operation -- Names must be UTS-46 normalized before hashing. "Alice.ETH" and "alice.eth" produce different hashes. viem normalizes automatically, but if you build raw calldata you must normalize first using@adraffy/ens-normalize.
Quick Start
Installation
npm install viem
viem has built-in ENS support -- no additional packages needed for resolution.
Forward Resolution (Name to Address)
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const address = await client.getEnsAddress({
name: "vitalik.eth",
});
// "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Reverse Resolution (Address to Name)
const name = await client.getEnsName({
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
});
// "vitalik.eth" (or null if no primary name set)
Get Avatar
const avatar = await client.getEnsAvatar({
name: "vitalik.eth",
});
// HTTPS URL to avatar image, or null
Name Resolution
Forward Resolution with Coin Types
ENS can store addresses for any blockchain, not just Ethereum. Each chain has a SLIP-44 coin type.
const ethAddress = await client.getEnsAddress({
name: "vitalik.eth",
});
// BTC address (coin type 0)
const btcAddress = await client.getEnsAddress({
name: "vitalik.eth",
coinType: 0,
});
// Solana address (coin type 501)
const solAddress = await client.getEnsAddress({
name: "vitalik.eth",
coinType: 501,
});
Text Records
ENS text records store arbitrary key-value metadata. Standard keys are defined in ENSIP-5.
const twitter = await client.getEnsText({
name: "vitalik.eth",
key: "com.twitter",
});
const github = await client.getEnsText({
name: "vitalik.eth",
key: "com.github",
});
const email = await client.getEnsText({
name: "vitalik.eth",
key: "email",
});
const url = await client.getEnsText({
name: "vitalik.eth",
key: "url",
});
const description = await client.getEnsText({
name: "vitalik.eth",
key: "description",
});
// Avatar is also a text record (ENSIP-12 supports NFT references)
const avatarRecord = await client.getEnsText({
name: "vitalik.eth",
key: "avatar",
});
// Can be HTTPS URL, IPFS URI, or NFT reference like
// "eip155:1/erc721:0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/1234"
Standard Text Record Keys
| Key | Description |
|---|---|
email | Email address |
url | Website URL |
avatar | Avatar image (HTTPS, IPFS, or NFT reference) |
description | Short bio |
display | Display name (may differ from ENS name) |
com.twitter | Twitter/X handle |
com.github | GitHub username |
com.discord | Discord username |
org.telegram | Telegram handle |
notice | Contract notice text |
keywords | Comma-separated keywords |
header | Profile header/banner image |
Content Hash
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const RESOLVER_ABI = parseAbi([
"function contenthash(bytes32 node) view returns (bytes)",
]);
const node = namehash("vitalik.eth");
// First get the resolver address
const resolverAddress = await client.getEnsResolver({
name: "vitalik.eth",
});
const contenthash = await client.readContract({
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "contenthash",
args: [node],
});
// Encoded content hash (IPFS, Swarm, Arweave, etc.)
Batch Resolution with Multicall
import { createPublicClient, http, parseAbi } from "viem";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const RESOLVER_ABI = parseAbi([
"function addr(bytes32 node) view returns (address)",
"function text(bytes32 node, string key) view returns (string)",
]);
const node = namehash("vitalik.eth");
const resolverAddress = await client.getEnsResolver({
name: "vitalik.eth",
});
const results = await client.multicall({
contracts: [
{
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "addr",
args: [node],
},
{
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "text",
args: [node, "com.twitter"],
},
{
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "text",
args: [node, "com.github"],
},
{
address: resolverAddress,
abi: RESOLVER_ABI,
functionName: "text",
args: [node, "url"],
},
],
});
const [addr, twitter, github, url] = results.map((r) => r.result);
Registration
Commit-Reveal Process
ENS .eth registration uses a two-step commit-reveal to prevent frontrunning. You must wait at least 60 seconds between commit and register.
import {
createPublicClient,
createWalletClient,
http,
parseAbi,
encodePacked,
keccak256,
parseEther,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
const ETH_REGISTRAR_CONTROLLER =
"0x253553366Da8546fC250F225fe3d25d0C782303b" as const;
const CONTROLLER_ABI = parseAbi([
"function rentPrice(string name, uint256 duration) view returns (tuple(uint256 base, uint256 premium))",
"function available(string name) view returns (bool)",
"function makeCommitment(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) pure returns (bytes32)",
"function commit(bytes32 commitment) external",
"function register(string name, address owner, uint256 duration, bytes32 secret, address resolver, bytes[] data, bool reverseRecord, uint16 ownerControlledFuses) payable",
]);
const PUBLIC_RESOLVER = "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63" as const;
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
});
async function registerName(label: string, durationSeconds: bigint) {
// 1. Check availability
const isAvailable = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "available",
args: [label],
});
if (!isAvailable) throw new Error(`${label}.eth is not available`);
// 2. Get price
const rentPrice = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "rentPrice",
args: [label, durationSeconds],
});
// Add 10% buffer for price fluctuation during commit-reveal wait
const totalPrice =
((rentPrice.base + rentPrice.premium) * 110n) / 100n;
// 3. Generate secret (random 32 bytes)
const secret = keccak256(
encodePacked(["address", "uint256"], [account.address, BigInt(Date.now())])
);
// 4. Create commitment
const commitment = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "makeCommitment",
args: [
label,
account.address,
durationSeconds,
secret,
PUBLIC_RESOLVER,
[], // data (encoded resolver calls to set records at registration)
true, // reverseRecord (set as primary name)
0, // ownerControlledFuses (0 = no fuses)
],
});
// 5. Submit commitment
const commitHash = await walletClient.writeContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "commit",
args: [commitment],
});
await client.waitForTransactionReceipt({ hash: commitHash });
console.log("Commitment submitted. Waiting 60 seconds...");
// 6. Wait at least 60 seconds (minCommitmentAge)
await new Promise((resolve) => setTimeout(resolve, 65_000));
// 7. Register
const registerHash = await walletClient.writeContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "register",
args: [
label,
account.address,
durationSeconds,
secret,
PUBLIC_RESOLVER,
[],
true,
0,
],
value: totalPrice,
});
const receipt = await client.waitForTransactionReceipt({
hash: registerHash,
});
if (receipt.status !== "success") {
throw new Error("Registration transaction reverted");
}
console.log(`Registered ${label}.eth for ${durationSeconds / 31536000n} year(s)`);
return receipt;
}
// Register for 1 year (365 days in seconds)
await registerName("myname", 31536000n);
Renewal
const CONTROLLER_ABI_RENEW = parseAbi([
"function rentPrice(string name, uint256 duration) view returns (tuple(uint256 base, uint256 premium))",
"function renew(string name, uint256 duration) payable",
]);
async function renewName(label: string, durationSeconds: bigint) {
const rentPrice = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI_RENEW,
functionName: "rentPrice",
args: [label, durationSeconds],
});
// 5% buffer for price changes
const totalPrice = ((rentPrice.base + rentPrice.premium) * 105n) / 100n;
const hash = await walletClient.writeContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI_RENEW,
functionName: "renew",
args: [label, durationSeconds],
value: totalPrice,
});
const receipt = await client.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error("Renewal transaction reverted");
}
console.log(`Renewed ${label}.eth for ${durationSeconds / 31536000n} year(s)`);
return receipt;
}
Check Price Before Registering
async function getRegistrationCost(
label: string,
durationSeconds: bigint
): Promise<{ base: bigint; premium: bigint; total: bigint }> {
const rentPrice = await client.readContract({
address: ETH_REGISTRAR_CONTROLLER,
abi: CONTROLLER_ABI,
functionName: "rentPrice",
args: [label, durationSeconds],
});
return {
base: rentPrice.base,
premium: rentPrice.premium,
total: rentPrice.base + rentPrice.premium,
};
}
Working with Resolvers
Setting Text Records
import {
createPublicClient,
createWalletClient,
http,
parseAbi,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { namehash } from "viem/ens";
const PUBLIC_RESOLVER = "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63" as const;
const RESOLVER_ABI = parseAbi([
"function setText(bytes32 node, string key, string value) external",
"function setAddr(bytes32 node, address addr) external",
"function setAddr(bytes32 node, uint256 coinType, bytes value) external",
"function setContenthash(bytes32 node, bytes hash) external",
"function multicall(bytes[] data) external returns (bytes[])",
]);
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
});
const node = namehash("myname.eth");
// Set a single text record
const hash = await walletClient.writeContract({
address: PUBLIC_RESOLVER,
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "com.twitter", "myhandle"],
});
await client.waitForTransactionReceipt({ hash });
Batch Update Records with Multicall
Setting multiple records in a single transaction using the resolver's built-in multicall.
import { encodeFunctionData } from "viem";
const node = namehash("myname.eth");
const calls = [
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "com.twitter", "myhandle"],
}),
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "com.github", "mygithub"],
}),
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "url", "https://mysite.com"],
}),
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "email", "me@mysite.com"],
}),
encodeFunctionData({
abi: RESOLVER_ABI,
functionName: "setText",
args: [node, "avatar", "https://mysite.com/avatar.png"],
}),
];
const hash = await walletClient.writeContract({
address: PUBLIC_RESOLVER,
abi: RESOLVER_ABI,
functionName: "multicall",
args: [calls],
});
const receipt = await client.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error("Multicall record update reverted");
}
Setting the Primary Name (Reverse Record)
const REVERSE_REGISTRAR = "0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb" as const;
const REVERSE_ABI = parseAbi([
"function setName(string name) external returns (bytes32)",
]);
const hash = await walletClient.writeContract({
address: REVERSE_REGISTRAR,
abi: REVERSE_ABI,
functionName: "setName",
args: ["myname.eth"],
});
await client.waitForTransactionReceipt({ hash });
Subdomains
Creating an Onchain Subdomain
import { parseAbi } from "viem";
import { namehash, labelhash } from "viem/ens";
const ENS_REGISTRY = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" as const;
const REGISTRY_ABI = parseAbi([
"function setSubnodeRecord(bytes32 node, bytes32 label, address owner, address resolver, uint64 ttl) external",
"function owner(bytes32 node) view returns (address)",
"function resolver(bytes32 node) view returns (address)",
]);
const parentNode = namehash("myname.eth");
const subLabel = labelhash("sub");
const hash = await walletClient.writeContract({
address: ENS_REGISTRY,
abi: REGISTRY_ABI,
functionName: "setSubnodeRecord",
args: [
parentNode,
subLabel,
account.address, // owner of sub.myname.eth
PUBLIC_RESOLVER, // resolver
0n, // TTL
],
});
await client.waitForTransactionReceipt({ hash });
// sub.myname.eth now exists and points to PUBLIC_RESOLVER
Offchain Subdomains (CCIP-Read / ERC-3668)
Offchain subdomains let you issue unlimited subdomains without gas costs. The resolver responds with an OffchainLookup error that directs the client to a gateway URL. The gateway returns signed data that is verified onchain.
This is how services like cb.id (Coinbase), uni.eth (Uniswap), and lens.xyz work.
For offchain resolution, viem handles CCIP-Read transparently -- no client-side changes needed:
// Resolving an offchain subdomain works identically to onchain names
const address = await client.getEnsAddress({
name: "myuser.cb.id",
});
// viem automatically:
// 1. Calls resolver.resolve(...)
// 2. Catches OffchainLookup revert
// 3. Fetches from the gateway URL
// 4. Calls resolver with the gateway proof
// 5. Returns the verified address
const avatar = await client.getEnsAvatar({
name: "myuser.cb.id",
});
To build your own offchain resolver, implement ERC-3668:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IExtendedResolver} from "@ensdomains/ens-contracts/contracts/resolvers/profiles/IExtendedResolver.sol";
/// @notice Offchain resolver that delegates lookups to a gateway
/// @dev Implements ERC-3668 (CCIP-Read) and ENSIP-10 (wildcard resolution)
contract OffchainResolver is IExtendedResolver {
string public url;
address public signer;
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);
constructor(string memory _url, address _signer) {
url = _url;
signer = _signer;
}
/// @notice ENSIP-10 wildcard resolve entry point
function resolve(
bytes calldata name,
bytes calldata data
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = url;
revert OffchainLookup(
address(this),
urls,
data,
this.resolveWithProof.selector,
abi.encode(name, data)
);
}
/// @notice Callback that verifies the gateway signature
function resolveWithProof(
bytes calldata response,
bytes calldata extraData
) external view returns (bytes memory) {
// Verify signature from gateway matches expected signer
// Return decoded result
// Implementation depends on your signing scheme
}
}
Contract Addresses
Ethereum Mainnet. Last verified: 2025-03-01.
| Contract | Address | Purpose |
|---|---|---|
| ENS Registry | 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e | Core registry -- maps names to owners and resolvers |
| Public Resolver | 0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63 | Default resolver for address, text, contenthash, and ABI records |
| ETH Registrar Controller | 0x253553366Da8546fC250F225fe3d25d0C782303b | Handles .eth name registration and renewal (commit-reveal) |
| Name Wrapper | 0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401 | Wraps names as ERC-1155 tokens with permission fuses |
| Reverse Registrar | 0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb | Manages reverse records (address-to-name mapping) |
| Base Registrar (NFT) | 0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85 | ERC-721 NFT for .eth second-level names |
| Universal Resolver | 0xce01f8eee7E30F8E3BfC1C22bCBc01faBc8680E4 | Batch resolution with CCIP-Read support |
Address Constants for TypeScript
const ENS_ADDRESSES = {
registry: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e",
publicResolver: "0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63",
ethRegistrarController: "0x253553366Da8546fC250F225fe3d25d0C782303b",
nameWrapper: "0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401",
reverseRegistrar: "0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7Cb",
baseRegistrar: "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85",
universalResolver: "0xce01f8eee7E30F8E3BfC1C22bCBc01faBc8680E4",
} as const satisfies Record<string, `0x${string}`>;
Sepolia Testnet
Last verified: 2025-03-01.
| Contract | Address |
|---|---|
| ENS Registry | 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e |
| Public Resolver | 0x8FADE66B79cC9f707aB26799354482EB93a5B7dD |
| ETH Registrar Controller | 0xFED6a969AaA60E4961FCD3EBF1A2e8913DeBe6c7 |
| Name Wrapper | 0x0635513f179D50A207757E05759CbD106d7dFcE8 |
| Reverse Registrar | 0xA0a1AbcDAe1a2a4A2EF8e9113Ff0e02DD81DC0C6 |
Error Handling
Common Resolution Errors
async function safeResolve(name: string) {
try {
const address = await client.getEnsAddress({ name });
if (!address) {
console.log(`${name} has no address record set`);
return null;
}
return address;
} catch (error) {
if (error instanceof Error) {
// Name does not exist or is malformed
if (error.message.includes("Could not find resolver")) {
console.log(`${name} is not registered or has no resolver`);
return null;
}
// CCIP-Read gateway failure
if (error.message.includes("OffchainLookup")) {
console.log(`Offchain resolution failed for ${name}`);
return null;
}
}
throw error;
}
}
Common Registration Errors
| Error | Cause | Fix |
|---|---|---|
CommitmentTooNew | Called register() less than 60s after commit() | Wait at least 60 seconds between commit and register |
CommitmentTooOld | Commitment expired (older than 24 hours) | Submit a new commitment |
NameNotAvailable | Name is registered or in grace period | Check available() first |
DurationTooShort | Duration under minimum (28 days) | Use at least 2419200 seconds |
InsufficientValue | Sent less ETH than rentPrice() requires | Add a 5-10% buffer to rentPrice() result |
Unauthorised | Caller is not the name owner | Verify ownership via registry before writing records |
Validating ENS Names
import { normalize } from "viem/ens";
function isValidEnsName(name: string): boolean {
try {
normalize(name);
return true;
} catch {
return false;
}
}
// normalize() throws on invalid names
// Valid: "alice.eth", "sub.alice.eth", "alice.xyz"
// Invalid: names with zero-width characters, confusable Unicode, etc.
Key Constants
| Constant | Value | Notes |
|---|---|---|
| Min commitment age | 60 seconds | Wait between commit and register |
| Max commitment age | 86400 seconds (24h) | Commitment expires after this |
| Min registration duration | 2419200 seconds (28 days) | Shortest allowed registration |
| Grace period | 90 days | After expiry, owner can still renew |
| Premium auction | 21 days | After grace period, decaying price auction |
Namehash of eth | 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae | Used as parent node for .eth names |
References
- ENS Documentation -- official docs covering architecture, resolution, registration, and CCIP-Read
- EIP-137: ENS -- core ENS specification (registry, namehash, resolvers)
- EIP-181: Reverse Resolution -- reverse registrar and addr.reverse namespace
- EIP-2304: Multichain Address Resolution -- SLIP-44 coin type support in resolvers
- ERC-3668: CCIP-Read -- offchain data retrieval standard
- ENSIP-5: Text Records -- standardized text record keys
- ENSIP-10: Wildcard Resolution -- dynamic subdomain resolution
- ENSIP-12: Avatar Text Records -- NFT and IPFS avatar specification
- ENS Deployments -- official contract addresses per network
- viem ENS Actions -- built-in ENS resolution in viem
- @adraffy/ens-normalize -- reference UTS-46 normalization library used by viem
pyth
View full →Author
@0xinit
Stars
53
Repository
0xinit/cryptoskills
Pyth Network Development Guide
Pyth Network is a decentralized oracle providing real-time price feeds for cryptocurrencies, equities, forex, and commodities. This guide covers integrating Pyth price feeds into Solana applications.
Overview
Pyth Network provides:
- Real-Time Price Feeds - 400ms update frequency with pull oracle model
- Confidence Intervals - Statistical uncertainty bounds for each price
- EMA Prices - Exponential moving average prices (~1 hour window)
- Multi-Asset Support - Crypto, equities, FX, commodities, indices
- On-Chain Integration - CPI for Solana programs
- Off-Chain Integration - HTTP and WebSocket APIs via Hermes
Program IDs
| Program | Address | Description |
|---|---|---|
| Solana Receiver | rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ | Posts price updates to Solana |
| Price Feed | pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT | Stores price feed data |
Deployed on: Solana Mainnet, Devnet, Eclipse Mainnet/Testnet, Sonic networks
Popular Price Feed IDs
| Asset | Hex Feed ID |
|---|---|
| BTC/USD | 0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43 |
| ETH/USD | 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace |
| SOL/USD | 0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d |
| USDC/USD | 0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a |
| USDT/USD | 0x2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b |
Full list: https://pyth.network/developers/price-feed-ids
Quick Start
Installation
# TypeScript/JavaScript
npm install @pythnetwork/hermes-client @pythnetwork/pyth-solana-receiver
# Rust (add to Cargo.toml)
# pyth-solana-receiver-sdk = "0.3.0"
Fetch Price (Off-Chain)
import { HermesClient } from "@pythnetwork/hermes-client";
const client = new HermesClient("https://hermes.pyth.network");
const priceIds = [
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", // BTC/USD
];
const priceUpdates = await client.getLatestPriceUpdates(priceIds);
for (const update of priceUpdates.parsed) {
const price = update.price;
const displayPrice = Number(price.price) * Math.pow(10, price.expo);
console.log(`Price: $${displayPrice.toFixed(2)}`);
console.log(`Confidence: ±${Number(price.conf) * Math.pow(10, price.expo)}`);
}
Use Price On-Chain (Rust/Anchor)
use anchor_lang::prelude::*;
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;
#[derive(Accounts)]
pub struct UsePrice<'info> {
pub price_update: Account<'info, PriceUpdateV2>,
}
pub fn use_price(ctx: Context<UsePrice>) -> Result<()> {
let price_update = &ctx.accounts.price_update;
let clock = Clock::get()?;
// Get price no older than 60 seconds
let price = price_update.get_price_no_older_than(
&clock,
60, // max age in seconds
)?;
msg!("Price: {} × 10^{}", price.price, price.exponent);
msg!("Confidence: ±{}", price.conf);
Ok(())
}
Core Concepts
Price Structure
Each Pyth price contains:
| Field | Type | Description |
|---|---|---|
price | i64 | Price value in fixed-point format |
conf | u64 | Confidence interval (standard deviation) |
expo | i32 | Exponent for scaling (e.g., -8 means divide by 10^8) |
publish_time | i64 | Unix timestamp of price |
Converting to display price:
const displayPrice = price * Math.pow(10, expo);
// Example: price=19405100, expo=-2 → $194,051.00
Confidence Intervals
Confidence intervals represent the uncertainty in the reported price:
// Price is $50,000 ± $50 means:
// - 68% chance true price is between $49,950 - $50,050
// - Use confidence for risk management
const price = 50000;
const confidence = 50;
// Safe lower bound (conservative)
const safeLowerBound = price - confidence;
// Safe upper bound (conservative)
const safeUpperBound = price + confidence;
Best Practice: Reject prices with confidence > 2% of price:
const maxConfidenceRatio = 0.02; // 2%
const confidenceRatio = confidence / Math.abs(price);
if (confidenceRatio > maxConfidenceRatio) {
throw new Error("Price confidence too wide");
}
EMA Prices
Exponential Moving Average prices smooth out short-term volatility:
- ~1 hour averaging window (5921 Solana slots)
- Weighted by inverse confidence (tight confidence = more weight)
- Good for: liquidations, collateral valuation
- Available as
ema_priceandema_conf
// Use EMA for less volatile applications
const emaPrice = priceUpdate.emaPrice;
const emaConf = priceUpdate.emaConf;
Off-Chain Integration
Hermes Client
Hermes is the recommended way to fetch Pyth prices off-chain.
Public Endpoint: https://hermes.pyth.network
For production, get a dedicated endpoint from a Pyth data provider.
Fetching Latest Prices
import { HermesClient } from "@pythnetwork/hermes-client";
const client = new HermesClient("https://hermes.pyth.network");
// Single price
const btcPrice = await client.getLatestPriceUpdates([
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
]);
// Multiple prices in one request
const prices = await client.getLatestPriceUpdates([
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", // BTC
"0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", // ETH
"0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", // SOL
]);
Streaming Real-Time Updates
import { HermesClient } from "@pythnetwork/hermes-client";
const client = new HermesClient("https://hermes.pyth.network");
const priceIds = [
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
];
// Subscribe to real-time updates via SSE
const eventSource = await client.getPriceUpdatesStream(priceIds, {
parsed: true,
});
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Price update:", data);
};
eventSource.onerror = (error) => {
console.error("Stream error:", error);
eventSource.close();
};
// Close when done
// eventSource.close();
Posting Prices to Solana
import { PythSolanaReceiver } from "@pythnetwork/pyth-solana-receiver";
import { HermesClient } from "@pythnetwork/hermes-client";
import { Connection, Keypair } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const wallet = Keypair.fromSecretKey(/* your key */);
const hermesClient = new HermesClient("https://hermes.pyth.network");
const pythReceiver = new PythSolanaReceiver({ connection, wallet });
// Fetch price update data
const priceUpdateData = await hermesClient.getLatestPriceUpdates([
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"
]);
// Build transaction to post price
const transactionBuilder = pythReceiver.newTransactionBuilder();
await transactionBuilder.addPostPriceUpdates(priceUpdateData.binary.data);
// Add your program instruction that uses the price
// transactionBuilder.addInstruction(yourInstruction);
// Send transaction
const transactions = await transactionBuilder.buildVersionedTransactions({
computeUnitPriceMicroLamports: 50000,
});
for (const tx of transactions) {
const sig = await connection.sendTransaction(tx);
console.log("Transaction:", sig);
}
On-Chain Integration (Rust)
Setup
Add to Cargo.toml:
[dependencies]
pyth-solana-receiver-sdk = "0.3.0"
anchor-lang = "0.30.1"
Reading Price in Anchor Program
use anchor_lang::prelude::*;
use pyth_solana_receiver_sdk::price_update::{PriceUpdateV2, get_feed_id_from_hex};
declare_id!("YourProgramId...");
// BTC/USD price feed ID
const BTC_USD_FEED_ID: &str = "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43";
#[program]
pub mod my_program {
use super::*;
pub fn check_price(ctx: Context<CheckPrice>) -> Result<()> {
let price_update = &ctx.accounts.price_update;
let clock = Clock::get()?;
// Verify this is the correct feed
let feed_id = get_feed_id_from_hex(BTC_USD_FEED_ID)?;
// Get price no older than 60 seconds
let price = price_update.get_price_no_older_than_with_custom_verification(
&clock,
60,
&feed_id,
ctx.accounts.price_update.to_account_info().owner,
)?;
msg!("BTC/USD Price: {} × 10^{}", price.price, price.exponent);
msg!("Confidence: ±{}", price.conf);
Ok(())
}
}
#[derive(Accounts)]
pub struct CheckPrice<'info> {
#[account(
constraint = price_update.to_account_info().owner == &pyth_solana_receiver_sdk::ID
)]
pub price_update: Account<'info, PriceUpdateV2>,
}
Using Price for Calculations
pub fn swap_with_oracle(
ctx: Context<SwapWithOracle>,
amount_in: u64,
) -> Result<()> {
let price_update = &ctx.accounts.price_update;
let clock = Clock::get()?;
// Get price with staleness check
let price = price_update.get_price_no_older_than(&clock, 30)?;
// Validate confidence (max 1% of price)
let conf_ratio = (price.conf as u128 * 10000) / (price.price.unsigned_abs() as u128);
require!(conf_ratio <= 100, ErrorCode::ConfidenceTooWide);
// Convert price to usable format
// price.price is in fixed-point with price.exponent
let price_scaled = if price.exponent >= 0 {
(price.price as u128) * 10_u128.pow(price.exponent as u32)
} else {
(price.price as u128) / 10_u128.pow((-price.exponent) as u32)
};
// Calculate output amount using oracle price
let amount_out = (amount_in as u128)
.checked_mul(price_scaled)
.ok_or(ErrorCode::MathOverflow)?
/ 1_000_000; // Adjust for decimals
msg!("Swap {} -> {} using price {}", amount_in, amount_out, price_scaled);
Ok(())
}
#[error_code]
pub enum ErrorCode {
#[msg("Price confidence interval too wide")]
ConfidenceTooWide,
#[msg("Math overflow")]
MathOverflow,
}
Multiple Price Feeds
#[derive(Accounts)]
pub struct Liquidation<'info> {
#[account(
constraint = collateral_price.to_account_info().owner == &pyth_solana_receiver_sdk::ID
)]
pub collateral_price: Account<'info, PriceUpdateV2>,
#[account(
constraint = debt_price.to_account_info().owner == &pyth_solana_receiver_sdk::ID
)]
pub debt_price: Account<'info, PriceUpdateV2>,
}
pub fn check_liquidation(ctx: Context<Liquidation>) -> Result<bool> {
let clock = Clock::get()?;
let collateral = ctx.accounts.collateral_price
.get_price_no_older_than(&clock, 60)?;
let debt = ctx.accounts.debt_price
.get_price_no_older_than(&clock, 60)?;
// Normalize to same exponent for comparison
let collateral_value = normalize_price(collateral.price, collateral.exponent);
let debt_value = normalize_price(debt.price, debt.exponent);
// Check if undercollateralized
let is_liquidatable = collateral_value < debt_value * 150 / 100; // 150% ratio
Ok(is_liquidatable)
}
fn normalize_price(price: i64, expo: i32) -> i128 {
let target_expo = -8; // Normalize to 8 decimals
let adjustment = expo - target_expo;
if adjustment >= 0 {
(price as i128) * 10_i128.pow(adjustment as u32)
} else {
(price as i128) / 10_i128.pow((-adjustment) as u32)
}
}
Best Practices
1. Always Check Staleness
// Don't use old prices - set appropriate max age
let max_age_seconds = 60;
let price = price_update.get_price_no_older_than(&clock, max_age_seconds)?;
2. Validate Confidence Intervals
// Reject prices with wide confidence (high uncertainty)
const MAX_CONF_BPS: u64 = 200; // 2%
let conf_bps = (price.conf as u128 * 10000) / (price.price.unsigned_abs() as u128);
require!(conf_bps <= MAX_CONF_BPS as u128, ErrorCode::ConfidenceTooWide);
3. Verify Account Ownership
// Always verify the price account is owned by Pyth
#[account(
constraint = price_update.to_account_info().owner == &pyth_solana_receiver_sdk::ID
)]
pub price_update: Account<'info, PriceUpdateV2>,
4. Use EMA for Sensitive Operations
// For liquidations, use EMA to avoid manipulation
let ema_price = price_update.get_ema_price_no_older_than(&clock, 60)?;
5. Handle Price Unavailability
try {
const price = await client.getLatestPriceUpdates([feedId]);
// Use price
} catch (error) {
// Fallback behavior or reject transaction
console.error("Price unavailable:", error);
}
6. Consider Frontrunning
- Adversaries may see price updates before your transaction
- Don't design logic that races against price updates
- Use appropriate slippage tolerances
Price Feed Types
Fixed Price Feed Accounts
- Maintained continuously by Pyth
- Fixed address per feed
- Always has most recent price
- Shared by all users (potential congestion)
Ephemeral Price Update Accounts
- Created per transaction
- Can specify shard ID for parallelization
- Rent can be recovered after use
- Better for high-throughput applications
// Use shard ID to avoid congestion
const transactionBuilder = pythReceiver.newTransactionBuilder({
shardId: Math.floor(Math.random() * 65536), // Random shard
});
Resources
Official Documentation
GitHub Repositories
NPM Packages
Rust Crates
Skill Structure
pyth/
├── SKILL.md # This file
├── resources/
│ ├── program-addresses.md # All program IDs and feed IDs
│ └── api-reference.md # SDK API reference
├── examples/
│ ├── price-feeds/
│ │ ├── fetch-price.ts # Basic price fetching
│ │ └── multiple-prices.ts # Multiple price feeds
│ ├── on-chain/
│ │ ├── anchor-integration.rs # Anchor program example
│ │ └── price-validation.rs # Price validation patterns
│ └── streaming/
│ └── real-time-updates.ts # WebSocket streaming
├── templates/
│ ├── pyth-client.ts # TypeScript client template
│ └── anchor-oracle.rs # Anchor program template
└── docs/
└── troubleshooting.md # Common issues and solutions
Pyth on EVM Chains
This skill covers Pyth integration for Solana applications using Anchor CPI. For EVM chain integration (Ethereum, Arbitrum, Base, Optimism, Polygon, and 50+ other chains), see the pyth-evm skill.
Key differences between Pyth Solana and Pyth EVM:
| Aspect | Pyth Solana (this skill) | Pyth EVM (pyth-evm skill) |
|---|---|---|
| Contract interface | Anchor CPI to Pyth program | Solidity IPyth interface |
| Price update | Pull from Pyth accumulator account | Submit bytes[] via updatePriceFeeds |
| Contract address | Single Pyth program on Solana | Varies per EVM chain |
| Gas/compute | Compute units | ~120-150K gas per feed update |
| SDK | @pythnetwork/pyth-solana-receiver | @pythnetwork/hermes-client v3.1.0 |
Price feed IDs (bytes32) are the same across all chains — a BTC/USD feed ID works on both Solana and Ethereum.
Related Skills
pyth-evm— Pyth oracle integration for EVM chains (Solidity + TypeScript)chainlink— Push oracle alternative on EVM chainsredstone— Another pull oracle for EVM chains