Alchemy SDK Patterns

SkillDev tools

Gives your agent ready-made code patterns for building Web3 apps with the Alchemy SDK.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Alchemy SDK Patterns skill

About this capability

Route Alchemy work to viem, direct Data APIs, Wallet APIs v5, or Solana Web3.js without reviving the archived SDK. Use when designing shared clients and adapters. Trigger with "Alchemy SDK pattern", "replace alchemy-sdk", or "choose an Alchemy client".

What this skill tells your AI

The instructions your AI receives, as published by jeremylongshore/tons-of-skills-marketplace in skills/.curated/alchemy-sdk-patterns/SKILL.md and read by ahel’s review.

Overview

Production patterns for the alchemy-sdk package: singleton clients, multi-chain factories, response caching, and type-safe contract wrappers.

Prerequisites

  • A server-side environment with a managed provider key and a typed list of supported chains; do not create browser-side clients with the key.
  • A freshness policy for each cached response type and a test fixture for both successful and failed provider calls.
  • Input validation at the application boundary before an address, collection, or network value reaches the client factory or query builder.

Instructions

Step 1: Multi-Chain Client Factory

// src/alchemy/client-factory.ts
import { Alchemy, Network } from 'alchemy-sdk';

type ChainName = 'ethereum' | 'polygon' | 'arbitrum' | 'optimism' | 'base';

const NETWORK_MAP: Record<ChainName, Network> = {
  ethereum: Network.ETH_MAINNET,
  polygon: Network.MATIC_MAINNET,
  arbitrum: Network.ARB_MAINNET,
  optimism: Network.OPT_MAINNET,
  base: Network.BASE_MAINNET,
};

class AlchemyClientFactory {
  private static clients = new Map<string, Alchemy>();

  static getClient(chain: ChainName): Alchemy {
    if (!this.clients.has(chain)) {
      this.clients.set(chain, new Alchemy({
        apiKey: process.env.ALCHEMY_API_KEY,
        network: NETWORK_MAP[chain],
        maxRetries: 3,
      }));
    }
    return this.clients.get(chain)!;
  }

  static getAllClients(): Map<ChainName, Alchemy> {
    for (const chain of Object.keys(NETWORK_MAP) as ChainName[]) {
      this.getClient(chain);
    }
    return this.clients as Map<ChainName, Alchemy>;
  }
}

export { AlchemyClientFactory, ChainName };

Step 2: Response Caching Layer

// src/alchemy/cache.ts
interface CacheEntry<T> { data: T; expiresAt: number; }

class AlchemyCache {
  private cache = new Map<string, CacheEntry<any>>();
  private defaultTtlMs: number;

  constructor(defaultTtlMs: number = 30000) { // 30s default
    this.defaultTtlMs = defaultTtlMs;
  }

  async getOrFetch<T>(key: string, fetcher: () => Promise<T>, ttlMs?: number): Promise<T> {
    const cached = this.cache.get(key);
    if (cached && cached.expiresAt > Date.now()) return cached.data;

    const data = await fetcher();
    this.cache.set(key, { data, expiresAt: Date.now() + (ttlMs || this.defaultTtlMs) });
    return data;
  }

  invalidate(keyPrefix: string): void {
    for (const key of this.cache.keys()) {
      if (key.startsWith(keyPrefix)) this.cache.delete(key);
    }
  }
}

// Usage with Alchemy
const cache = new AlchemyCache();

async function getCachedBalance(alchemy: Alchemy, address: string): Promise<string> {
  return cache.getOrFetch(
    `balance:${address}`,
    async () => {
      const balance = await alchemy.core.getBalance(address);
      return (parseInt(balance.toString()) / 1e18).toFixed(6);
    },
    15000 // 15s cache for balances
  );
}

export { AlchemyCache, getCachedBalance };

Step 3: Typed NFT Query Builder

// src/alchemy/nft-query.ts
import { Alchemy, NftOrdering } from 'alchemy-sdk';

class NftQueryBuilder {
  private alchemy: Alchemy;
  private _owner?: string;
  private _contracts: string[] = [];
  private _pageSize = 20;
  private _excludeFilters: string[] = [];

  constructor(alchemy: Alchemy) { this.alchemy = alchemy; }

  forOwner(address: string): this { this._owner = address; return this; }
  inCollection(contractAddress: string): this { this._contracts.push(contractAddress); return this; }
  pageSize(size: number): this { this._pageSize = size; return this; }
  excludeSpam(): this { this._excludeFilters.push('SPAM'); return this; }

  async execute() {
    if (!this._owner) throw new Error('Owner address required');

    return this.alchemy.nft.getNftsForOwner(this._owner, {
      contractAddresses: this._contracts.length > 0 ? this._contracts : undefined,
      pageSize: this._pageSize,
      excludeFilters: this._excludeFilters as any[],
    });
  }
}

// Usage:
// const nfts = await new NftQueryBuilder(alchemy)
//   .forOwner('vitalik.eth')
//   .excludeSpam()
//   .pageSize(50)
//   .execute();

Step 4: Error Classification

// src/alchemy/errors.ts
type AlchemyErrorType = 'rate_limit' | 'auth' | 'network' | 'invalid_params' | 'server' | 'unknown';

function classifyError(error: any): { type: AlchemyErrorType; retryable: boolean; message: string } {
  const status = error.response?.status || error.code;

  if (status === 429) return { type: 'rate_limit', retryable: true, message: 'Rate limit exceeded' };
  if (status === 401 || status === 403) return { type: 'auth', retryable: false, message: 'Invalid API key' };
  if (status >= 500) return { type: 'server', retryable: true, message: 'Alchemy server error' };
  if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') return { type: 'network', retryable: true, message: 'Network error' };
  if (error.message?.includes('invalid params')) return { type: 'invalid_params', retryable: false, message: error.message };
  return { type: 'unknown', retryable: false, message: error.message };
}

export { classifyError, AlchemyErrorType };

Output

  • Multi-chain client factory with lazy initialization
  • Response cache with configurable TTL
  • Type-safe NFT query builder pattern
  • Structured error classification for retry decisions

Examples

In a staging service, request a balance for a public test address twice through getCachedBalance, assert the second request is a cache hit within the approved TTL, then invalidate the prefix and verify the next request reaches the provider. Use the factory only for a configured chain and reject an unrecognized chain at the route boundary. Inject a 429 and a 401 into the error classifier to prove that only the rate-limit case is retryable. If a cached value breaches its freshness rule, a chain is not configured, or a key reaches a client artifact, disable the route and repair that boundary first.

Error Handling

FailureResponse
Provider rate limit or transient server errorUse bounded retry/backoff and retain a sanitized outcome metric.
Authentication or authorization failureStop retries, verify managed-secret configuration, and rotate a suspected exposure.
Invalid address, contract, or chain inputReject before creating a provider operation.
Cache contains stale or incompatible dataInvalidate it and refetch according to the declared freshness policy.

Resources

Next Steps

Apply patterns in alchemy-core-workflow-a for real portfolio tracking.

Signals

GitHub stars
3k
Forks
396
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
alchemy-sdk-patterns
Source
github.com/jeremylongshore/tons-of-skills-marketplace