DEV Community

Cover image for Stop Plumbing Crypto News APIs by Hand: Introducing cryptopanic-go
Igor
Igor

Posted on

Stop Plumbing Crypto News APIs by Hand: Introducing cryptopanic-go

Building a crypto-facing product in Go can begin with a deceptively small task: fetch a news feed, filter it for a few assets, and surface the result in a dashboard, worker, or command-line tool. The integration becomes much less small once the production concerns arrive. You need a versioned API path, authenticated requests, query encoding, typed decoding, deadlines, pagination, useful failures, and a safe policy for transient outages.

The upstream CryptoPanic API exposes a posts endpoint with filters for currencies, regions, content kind, and pagination, and it requires an authentication token for requests.2 That is an entirely reasonable HTTP API. The question for a Go team is whether each application should rebuild the same integration layer around it.

cryptopanic-go is a focused answer to that question: a production-oriented Go SDK for the CryptoPanic API. Rather than wrapping the API in a framework or inventing business abstractions, it supplies a small, typed, context-aware client that is appropriate for services, background workers, and CLI tools.1

“The SDK deliberately stays close to the public CryptoPanic API. It does not invent a portfolio schema, hide server-side plan rules, or force a framework on your application.” — cryptopanic-go README 1

This article shows what that design buys you, how to get started, and where the SDK fits in a production Go codebase. It discusses integration engineering, not trading strategy or financial advice.

The problem is not making one HTTP request

A raw net/http request is often the correct first prototype. But the moment a crypto-news feature becomes part of a real product, the surrounding code tends to spread across handlers, workers, and scripts. Each call site may have to reconstruct the API URL, serialize filters, decode a response, decide what to retry, and avoid logging secrets. Those are all solvable problems—but they are poor places to spend the same engineering effort repeatedly.

The API itself returns next, previous, and results in a posts response.2 A generic client leaves your application responsible for interpreting those values, including making sure an authentication token in an upstream URL never makes it into a log line. Likewise, upstream permissions and plan-gated parameters remain important even when your request code is concise. A useful SDK should reduce boilerplate without concealing those operational realities.

Integration concern What repetitive hand-written code must handle What cryptopanic-go provides
Request construction API plan paths, query encoding, and optional filters PostsQuery, typed constants, and configurable client options.1
Request lifetime Cancellation and an operation-specific deadline A context.Context on every network-facing method.1
Sensitive data Avoiding token leakage through URLs and errors Token redaction in pagination URLs and SDK errors.1
Transient failure Backoff, retry selection, and Retry-After handling An opt-in retry policy with capped exponential backoff and jitter.1
Response shape JSON structs, optional fields, and raw endpoint formats Typed post models, deliberate raw portfolio JSON, and raw RSS XML.1

The result is not “magic.” It is simply a clearer boundary: application code decides which data matters, while the client consistently handles the mechanics of asking for it.

Why cryptopanic-go is a good fit for Go services

The library is intentionally compact. It requires Go 1.22 or newer and uses only the Go standard library at runtime, so adopting it does not introduce a broad transitive dependency tree.1 A configured Client can also be shared safely between goroutines, which matches the normal shape of Go servers and workers.1

More importantly, it uses types to make the common path explicit. Instead of decoding unstructured maps, you receive models such as Post and PostsPage. Optional or plan-gated values are represented carefully, so your code has a natural reason to check whether data is actually present before relying on it.1 That is a better failure mode than assuming a field will always exist and discovering otherwise at runtime.

The library also chooses not to over-model the upstream API where the source contract is not stable enough. For example, Portfolio returns a PortfolioResponse containing json.RawMessage; the README explains that this avoids guessing at a complete portfolio schema that the public API does not define as stable.1 The RSS methods similarly return raw XML rather than pretending it is the same thing as a JSON news page.1 Those choices preserve flexibility for the application instead of locking it into a potentially inaccurate SDK abstraction.

Install it and make the first request

Install the package with Go's standard tooling:

go get github.com/tigusigalpa/cryptopanic-go
Enter fullscreen mode Exit fullscreen mode

The repository documents CRYPTOPANIC_AUTH_TOKEN as the environment variable used by NewFromEnv; keeping the token outside source control is the recommended starting point.1

export CRYPTOPANIC_AUTH_TOKEN="your-token"
Enter fullscreen mode Exit fullscreen mode

Here is a compact, production-shaped example. It creates one reusable client, configures a client timeout and retry policy, then applies a shorter deadline to this individual operation.

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    cryptopanic "github.com/tigusigalpa/cryptopanic-go"
 )

func main() {
    client, err := cryptopanic.NewFromEnv(
        cryptopanic.WithPlan(cryptopanic.PlanGrowth),
        cryptopanic.WithTimeout(10*time.Second),
        cryptopanic.WithRetryPolicy(cryptopanic.RetryPolicy{
            MaxAttempts: 3,
        }),
    )
    if err != nil {
        log.Fatal(err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    page, err := client.Posts(ctx, cryptopanic.PostsQuery{
        Currencies: []string{"BTC", "ETH"},
        Filter:     cryptopanic.FilterRising,
        Page:       cryptopanic.IntPtr(1),
    })
    if err != nil {
        log.Fatal(err)
    }

    for _, post := range page.Results {
        fmt.Printf("[%s] %s\n", post.Kind, post.Title)
    }
}
Enter fullscreen mode Exit fullscreen mode

This example follows the package's documented configuration model: choose the API plan explicitly, set the underlying client timeout deliberately, and give the request its own context deadline.1 The explicit plan matters because access and available parameters are ultimately determined by the upstream CryptoPanic account and plan rather than by the SDK itself.1

Filter without manually assembling query strings

The core Posts method accepts a PostsQuery. Empty fields are omitted from the request, while the client validates rules that are safe to check locally; the upstream service remains the authority on evolving account permissions and capabilities.1

That distinction is useful in real applications. You can write an intent-oriented query in Go and still keep the underlying API semantics visible in your code:

size := 20
pageNumber := 1

page, err := client.Posts(ctx, cryptopanic.PostsQuery{
    Currencies: []string{"BTC", "ETH", "SOL"},
    Regions:    "en",
    Kind:       cryptopanic.KindNews,
    Filter:     cryptopanic.FilterHot,
    Size:       &size,
    Page:       &pageNumber,
})
if err != nil {
    log.Fatal(err)
}

for _, post := range page.Results {
    fmt.Println(post.Title)
    if post.Source != nil {
        fmt.Println("source:", post.Source.Title)
    }
}
Enter fullscreen mode Exit fullscreen mode

The SDK exposes typed constants for commonly documented values such as filters, kinds, panic periods, panic sorting, and plans.1 The underlying API documents filters including currencies, regions, filter, kind, and page for its posts endpoint.2 That combination gives you good editor support without making the client falsely claim control over server-side entitlement rules.

Pagination that does not spread secret URLs through your code

Pagination is exactly the type of small detail that gets copied badly. CryptoPanic returns next and previous URLs in its response envelope.2 cryptopanic-go preserves the useful paging information while redacting the authentication token and extracting page numbers where possible. Its PostsPage exposes helpers such as HasNext, HasPrevious, NextPage, and PreviousPage.1

That makes a page-by-page retrieval loop direct and easy to review:

page, err := client.Posts(ctx, cryptopanic.PostsQuery{
    Page: cryptopanic.IntPtr(1),
})
if err != nil {
    log.Fatal(err)
}

for page.HasNext && page.NextPage != nil {
    next, err := client.Posts(ctx, cryptopanic.PostsQuery{
        Page: page.NextPage,
    })
    if err != nil {
        log.Fatal(err)
    }

    for _, post := range next.Results {
        fmt.Println(post.Title)
    }

    page = next
}
Enter fullscreen mode Exit fullscreen mode

The important design choice is that your application keeps sending a normal PostsQuery; it is not asked to treat an upstream pagination URL as an executable instruction. That is both easier to test and safer to log.

Treat errors as part of the interface

In production, a useful client does more than return a non-nil error. It tells your application whether the error is a configuration problem, an access problem, a rate-limit event, or a server-side failure.

cryptopanic-go provides sentinel errors that work with errors.Is, including ErrMissingAuthToken, ErrInvalidQuery, ErrUnauthorized, ErrForbidden, ErrRateLimited, and ErrServerError. It also exposes an APIError that can be inspected with errors.As for details such as the status code, request ID, and retry count.1

page, err := client.Posts(ctx, query)
if err != nil {
    switch {
    case errors.Is(err, cryptopanic.ErrUnauthorized):
        log.Println("Authentication failed; check the configured token.")
    case errors.Is(err, cryptopanic.ErrRateLimited):
        log.Println("The rate limit was reached and retries were exhausted.")
    case errors.Is(err, cryptopanic.ErrServerError):
        log.Println("The upstream service returned a server error.")
    }

    var apiErr *cryptopanic.APIError
    if errors.As(err, &apiErr) {
        fmt.Printf("status=%d request_id=%s retried=%d\n",
            apiErr.StatusCode,
            apiErr.RequestID,
            apiErr.Retried,
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

This style lets a worker send an operational signal for an invalid token, apply its own queuing policy after a rate limit, or preserve a request ID for troubleshooting. The SDK documents that its error details use a redacted, truncated raw body, which is a thoughtful safeguard when diagnostics are routed to centralized logging.1

Retry deliberately, not by default

Automatic retries are useful only when they are intentional. The library leaves retries disabled by default and makes them an explicit RetryPolicy choice.1 When enabled, its documented retry scope covers HTTP 429, 500, 502, and 503, plus selected transient network failures; permanent failures such as bad requests, unauthorized access, invalid URLs, and certificate failures are not retried.1

The policy uses exponential backoff with jitter, caps the delay, and respects an upstream Retry-After header within that cap.1 That is a sensible foundation for idempotent GET operations such as reading news. It does not remove the need for application-level discipline: use a context deadline, apply caching where your traffic pattern calls for it, and make sure a retry policy aligns with the workload you operate.

More than one endpoint, without pretending they are the same

The package currently provides a small surface area that maps to meaningful API operations.1

API task Go method Returned form Why it matters
Fetch filtered posts client.Posts(ctx, query) *PostsPage Typed news data and paging helpers support normal application flows.1
Read a portfolio response client.Portfolio(ctx) *PortfolioResponse with raw JSON Your application can decode an evolving schema on its own terms.1
Read filtered posts as RSS client.PostsRSS(ctx, query) *RSSResponse with raw XML RSS stays XML instead of being forced into JSON models.1
Read the news RSS feed client.NewsRSS(ctx) *RSSResponse with raw XML A simple option for feed-oriented integrations.1

This is a strong example of an SDK being useful without becoming large. It handles the repeated transport work, preserves the distinctions between the remote formats, and lets your application own its own product model.

Production-minded, testable, and easy to adopt

The project supports custom *http.Client values for proxies, transports, TLS configuration, and testing. Its WithBaseURL option is designed for mock servers, while WithSleeper supports deterministic retry tests.1 The repository's own tests use httptest.Server and fixtures rather than live API calls, a practical pattern worth retaining in your own test suite.1

For applications that need CryptoPanic data but do not want a broad framework or a pile of copied HTTP boilerplate, cryptopanic-go makes a compelling trade-off. You get a small Go-native interface, explicit configuration, typed data where the API schema is stable, raw data where it is not, and operational safeguards around timeouts, retries, and credentials.

If that matches your use case, start with the repository, run the examples, and adapt the query and error-handling patterns to your service. You can find the source, documentation, and contribution guidance on GitHub.1

Try it: go get github.com/tigusigalpa/cryptopanic-go and build the integration code your product actually needs—not another layer of hand-rolled request plumbing.

References

Top comments (0)