Comparing ens with pyth

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/ens/SKILL.md

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 recursive namehash algorithm (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 use namehash() and labelhash() 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 .eth name registration and renewal. Confusing these is the most common ENS integration bug.
  • .eth registrar uses commit-reveal, not a single transaction -- Registration requires two transactions separated by at least 60 seconds: first commit(secret), wait, then register(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 null returns from getEnsName().
  • 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 isWrapped before assuming standard ownership patterns.
  • CCIP-Read (ERC-3668) enables offchain resolution -- Resolvers can return an OffchainLookup error 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 -- .eth names 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

KeyDescription
emailEmail address
urlWebsite URL
avatarAvatar image (HTTPS, IPFS, or NFT reference)
descriptionShort bio
displayDisplay name (may differ from ENS name)
com.twitterTwitter/X handle
com.githubGitHub username
com.discordDiscord username
org.telegramTelegram handle
noticeContract notice text
keywordsComma-separated keywords
headerProfile 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.

ContractAddressPurpose
ENS Registry0x00000000000C2E074eC69A0dFb2997BA6C7d2e1eCore registry -- maps names to owners and resolvers
Public Resolver0x231b0Ee14048e9dCcD1d247744d114a4EB5E8E63Default resolver for address, text, contenthash, and ABI records
ETH Registrar Controller0x253553366Da8546fC250F225fe3d25d0C782303bHandles .eth name registration and renewal (commit-reveal)
Name Wrapper0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401Wraps names as ERC-1155 tokens with permission fuses
Reverse Registrar0xa58E81fe9b61B5c3fE2AFD33CF304c454AbFc7CbManages reverse records (address-to-name mapping)
Base Registrar (NFT)0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85ERC-721 NFT for .eth second-level names
Universal Resolver0xce01f8eee7E30F8E3BfC1C22bCBc01faBc8680E4Batch 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.

ContractAddress
ENS Registry0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e
Public Resolver0x8FADE66B79cC9f707aB26799354482EB93a5B7dD
ETH Registrar Controller0xFED6a969AaA60E4961FCD3EBF1A2e8913DeBe6c7
Name Wrapper0x0635513f179D50A207757E05759CbD106d7dFcE8
Reverse Registrar0xA0a1AbcDAe1a2a4A2EF8e9113Ff0e02DD81DC0C6

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

ErrorCauseFix
CommitmentTooNewCalled register() less than 60s after commit()Wait at least 60 seconds between commit and register
CommitmentTooOldCommitment expired (older than 24 hours)Submit a new commitment
NameNotAvailableName is registered or in grace periodCheck available() first
DurationTooShortDuration under minimum (28 days)Use at least 2419200 seconds
InsufficientValueSent less ETH than rentPrice() requiresAdd a 5-10% buffer to rentPrice() result
UnauthorisedCaller is not the name ownerVerify 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

ConstantValueNotes
Min commitment age60 secondsWait between commit and register
Max commitment age86400 seconds (24h)Commitment expires after this
Min registration duration2419200 seconds (28 days)Shortest allowed registration
Grace period90 daysAfter expiry, owner can still renew
Premium auction21 daysAfter grace period, decaying price auction
Namehash of eth0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4aeUsed as parent node for .eth names

References

Author

@0xinit

Stars

53

Repository

0xinit/cryptoskills

skills/pyth/SKILL.md

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

ProgramAddressDescription
Solana Receiverrec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJPosts price updates to Solana
Price FeedpythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsTStores price feed data

Deployed on: Solana Mainnet, Devnet, Eclipse Mainnet/Testnet, Sonic networks

Popular Price Feed IDs

AssetHex Feed ID
BTC/USD0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43
ETH/USD0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
SOL/USD0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d
USDC/USD0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a
USDT/USD0x2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b

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:

FieldTypeDescription
pricei64Price value in fixed-point format
confu64Confidence interval (standard deviation)
expoi32Exponent for scaling (e.g., -8 means divide by 10^8)
publish_timei64Unix 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_price and ema_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:

AspectPyth Solana (this skill)Pyth EVM (pyth-evm skill)
Contract interfaceAnchor CPI to Pyth programSolidity IPyth interface
Price updatePull from Pyth accumulator accountSubmit bytes[] via updatePriceFeeds
Contract addressSingle Pyth program on SolanaVaries per EVM chain
Gas/computeCompute 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 chains
  • redstone — Another pull oracle for EVM chains

AI Skill Finder

Ask me what skills you need

What are you building?

Tell me what you're working on and I'll find the best agent skills for you.