All guides

Engineering Guide

How to Extend a Fomo API Integration Safely

A complete guide to organizing, testing and extending your Fomo API client with typed models, adapters, observability and contribution standards.

By Fomo API 4 min read

A first integration can be one script. A maintainable integration needs boundaries that let developers add endpoints, networks and storage without risking existing behavior. This guide shows a practical structure for a team project.

Start with clear modules

src/
  config.ts
  fomo-client.ts
  schemas.ts
  normalizers.ts
  repositories.ts
  poller.ts
  signals.ts
  risk.ts
  index.ts
tests/
  fixtures/
  client.test.ts
  normalizers.test.ts

The API client should understand HTTP but not your database. Normalizers should be pure functions. Repositories should know persistence but not request headers. Signal and risk modules should consume normalized domain records.

Validate configuration once

Fail during process startup rather than during the first scheduled poll:

function required(name: string) {
  const value = process.env[name]?.trim()
  if (!value) throw new Error(`${name} is required`)
  return value
}

export const config = {
  fomoApiKey: required("FOMO_API_KEY"),
  fomoBaseUrl: process.env.FOMO_BASE_URL ?? "https://getfomoapi.fun/api",
  requestTimeoutMs: 30_000,
}

Never accept an API key as a request parameter from frontend users. Keep it in the server environment and redact headers from logs.

Add runtime schemas

TypeScript types disappear at runtime. Validate responses before inserting them into trusted tables. A small schema library can distinguish required identifiers from optional display fields.

type Trader = {
  id: string
  userHandle: string
  solana: string | null
  evm: string | null
}

function isTrader(value: unknown): value is Trader {
  if (!value || typeof value !== "object") return false
  const item = value as Record<string, unknown>
  return typeof item.id === "string" && typeof item.userHandle === "string"
}

For a larger client, use a maintained schema validator and preserve unknown fields in raw storage.

Design endpoint methods consistently

Every endpoint method should:

  1. Validate caller inputs.
  2. Construct the path and query safely.
  3. Use the shared authenticated request function.
  4. Validate the response envelope.
  5. Return endpoint data, not transport details, unless callers need headers.
async function getSwaps(userId: string, limit = 20) {
  if (!userId) throw new Error("userId is required")
  if (limit < 1 || limit > 150) throw new Error("limit must be 1–150")

  return fomoGet(`/users/${encodeURIComponent(userId)}/swaps?limit=${limit}`)
}

Test with recorded fixtures

Store sanitized JSON fixtures for success, nullable fields, empty arrays, pagination, rate limiting and malformed responses. Never commit real API keys or private customer data.

Pure normalizer tests should cover:

CaseExpected behavior
Missing profile imagePreserve null; do not reject trader
Unknown networkStore raw event; exclude from execution
Duplicate swap IDNo second state transition
Decimal price stringParse with decimal-safe library
Empty leaderboardReturn an empty list, not an exception

Integration tests can run against a mock HTTP server. Keep live API tests separate so CI is deterministic and does not consume rate limits.

Add observability before features

Record structured fields such as endpoint, duration, status, attempt, user ID and checkpoint. Do not log authentication headers or full wallet profiles by default.

Useful metrics include:

  • Requests and errors by endpoint
  • Rate-limit responses
  • Retry count and total retry delay
  • Poll checkpoint age
  • New and duplicate swap count
  • Normalization failures
  • Signal rejection reason
  • Execution confirmation time

Alert on stale checkpoints, not only server errors. A poller can return 200 while repeatedly processing no new data because its cursor is broken.

Contribution workflow

Before opening a change:

  1. Create a focused branch.
  2. Add or update sanitized fixtures.
  3. Implement the smallest endpoint or normalizer change.
  4. Run formatting, lint, type checks and tests.
  5. Document new configuration and migration steps.
  6. Explain security and compatibility implications in the review.

Avoid combining an endpoint addition with an unrelated framework migration. Small changes are easier to review and roll back.

Adding a new network

Create an explicit network registry containing the chain ID, address validator, native asset, RPC adapter and execution support level. “Readable” and “executable” should be different states.

const networks = {
  1399811149: { name: "Solana", execution: "enabled" },
  8453: { name: "Base", execution: "review" },
} as const

An unknown network should never fall through to a default executor.

Compatibility and migrations

If normalized database records change, version the transformation or write a migration. Keep raw responses so you can backfill. Add compatibility logic only for data or consumers that actually exist; speculative compatibility increases ambiguity.

Definition of done

A safe extension includes code, runtime validation, fixtures, unit tests, observability, documentation and a rollout plan. If the feature can affect transactions, it also needs risk-engine rules, dry-run behavior and a kill switch.