Nebula Go SDK: A Type-Safe Go Client for Market Intelligence APIs
Every market has two stories: the one printed on the price chart, and the one people tell each other. Prices record what already happened. Sentiment, mindshare, and prediction markets hint at what the crowd expects next. The Nebula API from Hidden Systems serves both stories as structured data, and nebula-go is the Go client that makes it pleasant to consume.
If you have ever wired a market-data endpoint into a Go service, you know the drill. You write an HTTP client, invent a struct for half the response, keep the other half as map[string]any, add retries, forget to respect Retry-After, and then repeat the whole ritual for the next endpoint. nebula-go is an MIT-licensed attempt to delete that ritual. It covers all 41 operations of the Nebula Public API with typed requests and responses, a real error taxonomy, retries that listen to the server, server-sent-event streaming for the Nebula Agent, and an optional adapter for x402 pay-per-request billing.
In this overview you will see what the SDK covers, how its API feels in practice, which design decisions make it pleasant to live with, and where the sharp edges are.
In this article:
What Nebula brings to the table
Meet nebula-go: the essentials
Quick start: from zero to sentiment in one file
Design decisions that matter in production
Forecasts and prediction markets
Pay per request with x402, carefully
Generated from a contract, not from memory
FAQ
What Nebula brings to the table
Nebula is a market-intelligence API that watches the public conversation around financial assets — equities, commodities, indices, crypto, and AI-related names — and turns it into queryable signals. Instead of scraping social platforms and stitching together your own sentiment pipeline, you ask the API a question and get typed observations back.
The surface is broader than a single "sentiment endpoint":
| You want to know | Where to look |
|---|---|
| What is trending or gaining momentum right now? | Discovery — screener, top momentum, top trending |
| How is sentiment around an asset changing? | Social — sentiment, emotions, mindshare, related subjects |
| Which communities are driving the conversation? | Clusters — profiles, comparisons, overlap, rotation, stances |
| What do prediction markets imply? | Predictions — markets, venue listings, outcome history |
| What might mindshare look like in 72 hours? | Forecasts — mindshare and sentiment forecasts |
| What are the crowd's calls, conviction, and fear/greed? | MarketSentiment — calls, returns, conviction, fear/greed, price levels |
| What does an AI make of all this? | Agent — OpenAI-compatible chat, streamed or complete |
The same data is also exposed through a Nebula MCP server and a CLI, which makes the API a natural fit for coding agents and terminal workflows. But if you are writing a Go service, the REST API is the integration point — and that is exactly where nebula-go lives.
Meet nebula-go: the essentials
The project is young. The first version landed in late October 2026, and the changelog already lists five versions, from the initial 0.1.0 to 0.3.2 in mid-November. What it lacks in age it makes up for in discipline: the repository reads like someone sat down and decided to get the boring parts right.
| Fact | Detail |
|---|---|
| Module | github.com/tigusigalpa/nebula-go |
| Current release | v0.3.2 (November 2026) |
| Go version | 1.24 or newer |
| License | MIT |
| API coverage | 41 of 41 operations (40 GET, 1 POST) |
| Core dependencies | Go standard library only |
| Optional package | x402 — official x402 v2 payment client adapter |
| Docs | README, pkg.go.dev, API coverage matrix, generation guide |
The feature list is longer than the version number suggests:
Typed DTOs generated from OpenAPI. Requests are ordinary structs, responses have concrete field types all the way down to nested time-series points.
context.Context** on every call.** Timeouts and cancellation behave the way Go developers expect, including during retries and streamed responses.Retries with server hints. Eligible GETs retry on
429,500,502,503, and504, honoringRetry-AfterandRateLimit-Resetbefore falling back to exponential backoff with jitter.An inspectable error hierarchy. Sentinel errors for
errors.Is, rich*APIErrorvalues forerrors.As, and client-side validation before a request ever hits the network.SSE streaming for the Agent.
ChatStreamexposes a channel of chunks and a terminalErr()so you can tell a completed answer from a truncated one.Response metadata everywhere. Request ID, rate-limit figures, credits charged, credits remaining, and payment receipts are available on every response.
Localization. A global locale and per-request overrides, with constants for Spanish, Italian, Turkish, Russian, Korean, Japanese, and Chinese.
Opt-in x402 payments. A separate adapter for paying per request in USDC on Base, with explicit spend controls and a no-wallet preview mode.
An offline generator.
go generate ./...rebuilds the typed surface from the pinned OpenAPI contract; a-checkmode detects drift without writing files.
That last point deserves emphasis. Many SDKs are handwritten once and slowly drift from the API they wrap. This one treats the OpenAPI document as the source of truth and keeps the generated surface, the handwritten business rules, and the tests in separate layers. We will come back to that.
Quick start: from zero to sentiment in one file
Install the module from your project directory:
go get github.com/tigusigalpa/nebula-go Put your API key in an environment variable — never in source control or browser code:
# macOS / Linux export NEBULA_API_KEY='your-api-key' # Windows PowerShell $env:NEBULA_API_KEY = 'your-api-key' Now ask a small question: "What does the recent conversation around NVIDIA look like?" Save this as main.go and run go run .. It makes two account-mode API calls, so normal API credits apply.
package main import ( "context" "fmt" "log" "os" "time" nebula "github.com/tigusigalpa/nebula-go" ) func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { client, err := nebula.New(os.Getenv("NEBULA_API_KEY")) if err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() assets, err := client.Lookup.ListAssets(ctx, &nebula.ListAssetsRequest{ Query: nebula.StringPtr("NVIDIA"), }) if err != nil { return err } if len(assets.Assets) == 0 { fmt.Println("No matching assets") return nil } first := assets.Assets[0] fmt.Printf("Using %s (%s, %s)\n", first.Name, first.AssetID, first.AssetClass) asset := nebula.AssetRef{ID: first.AssetID, Class: first.AssetClass} sentiment, err := client.Social.Sentiment(ctx, &nebula.SentimentRequest{ Asset: &asset, Hours: nebula.IntPtr(168), }) if err != nil { return err } fmt.Printf("Received %d observations\n", len(sentiment.Series)) if len(sentiment.Series) > 0 { latest := sentiment.Series[len(sentiment.Series)-1] fmt.Printf("Latest signal: %.2f at %s\n", latest.SentimentSignal, latest.Timestamp.Format(time.RFC3339)) } if remaining := sentiment.HTTPMetadata().CreditsRemaining; remaining != nil { fmt.Printf("Credits left: %d\n", *remaining) } return nil } A few things are worth noticing in that small program.
The first call resolves a human-readable name to an API identifier. The second call carries both the AssetID and the AssetClass forward in an AssetRef. That pairing matters: a display symbol alone may not identify the asset you mean, especially in crypto, where several tokens can share a ticker. The SDK's type system nudges you to keep the identifier and its class together instead of passing loose strings around.
The sentiment signal is a decay-weighted bullish-minus-bearish measure. Positive values lean bullish, negative values lean bearish, and the magnitude also depends on activity. Read it alongside the timestamps and post counts rather than as a standalone trading signal.
Finally, HTTPMetadata() is available on the response even though the JSON body has its own metadata field. It reports the request ID, rate-limit state, and credit accounting for the call you just made. That is the kind of detail that usually gets bolted on after the first production incident.
Design decisions that matter in production
The API surface is only half of an SDK. The other half is how it behaves when the network misbehaves, the server pushes back, or the data is missing. This is where nebula-go shows its personality.
Pointers that preserve meaning
Optional request fields are pointers, so the SDK can distinguish "use the server default" from "send this exact value":
| Request value | What it means |
|---|---|
Limit: nil | Omit the parameter; let the server choose |
Limit: nebula.IntPtr(25) | Ask for 25 results |
Offset: nebula.IntPtr(0) | Explicitly start at the first result |
Refresh: nebula.BoolPtr(false) | Explicitly disable forecast refresh |
Helpers like StringPtr, IntPtr, Int64Ptr, Float64Ptr, BoolPtr, TimePtr, and typed-constant pointers keep the call sites readable. Explicit values still have to satisfy endpoint rules — Limit: IntPtr(0) is rejected for asset listings — but the SDK checks required fields, selector conflicts, enums, and numeric ranges before the HTTP call and returns a ValidationError that names the operation and the field.
The same philosophy applies to responses. Nullable scalars are pointers, so an unavailable price is nil, not a fake zero. That sounds pedantic until you build a chart and a missing data point silently becomes a crash to zero.
Errors that survive production
Errors are classified with sentinels and enriched with details. Application code can branch with errors.Is, while diagnostics use errors.As:
account, err := client.Account.Get(ctx) if err != nil { switch { case errors.Is(err, nebula.ErrValidation): var validationErr *nebula.ValidationError if errors.As(err, &validationErr) { return fmt.Errorf("fix %s: %s: %w", validationErr.Field, validationErr.Msg, err) } case errors.Is(err, nebula.ErrUnauthorized): return fmt.Errorf("check NEBULA_API_KEY: %w", err) case errors.Is(err, nebula.ErrOutOfCredits): return fmt.Errorf("account credits exhausted: %w", err) case errors.Is(err, context.DeadlineExceeded): return fmt.Errorf("request deadline reached: %w", err) case errors.Is(err, context.Canceled): return err } var apiErr *nebula.APIError if errors.As(err, &apiErr) { fmt.Printf("HTTP %d; retry hint %s; request %s\n", apiErr.StatusCode, apiErr.RetryAfter, apiErr.Metadata.RequestID) } return err } fmt.Println(account.Plan) The sentinel list covers the cases you actually branch on: validation, transport, bad request, unauthorized, out of credits, forbidden, not found, rate limited, server errors, and payment states. APIError keeps the status code, API message, headers, parsed metadata, and up to 8 KiB of the response body, which is usually enough to understand a failed call without reproducing it. A credit-related 402 preserves the plan renewal time and purchased-credit balance when the API supplies them.
There is also a deliberate distinction between failure modes: TransportError wraps network problems, DecodeError reports a successful HTTP response with invalid JSON, and wrapped errors preserve context cancellation. That means a canceled request does not get misclassified as a server failure in your metrics.
Retries that listen to the server
Retrying blindly is how you turn a rate limit into an outage. The SDK retries eligible GETs on transient network errors and HTTP 429, 500, 502, 503, or 504, and it picks the delay in a sensible order: Retry-After first, then RateLimit-Reset, then exponential backoff with jitter. Computed backoff is capped, and the overall call deadline still wins.
Just as important is what it does not retry: POST chat requests, x402 payments, other 4xx errors, decoding failures, and failed body reads. Those are exactly the cases where an automatic retry can duplicate work or spend money twice. If you add your own retry loop on top, you are multiplying attempts — the README says so plainly, which is refreshing.
Configuration stays out of the way until you need it:
client, err := nebula.New(os.Getenv("NEBULA_API_KEY"), nebula.WithTimeout(45*time.Second), nebula.WithRetry(3, 500*time.Millisecond, 5*time.Second), nebula.WithLocale(nebula.LocaleRussian), nebula.WithAuthMode(nebula.AuthModeBearer), ) if err != nil { return err } The defaults are reasonable: 30 seconds per call, three total GET attempts, 250 ms initial backoff with a 5-second cap. WithHTTPClient lets you inject your own proxy, TLS settings, or instrumentation. The client is immutable after construction and safe for concurrent use across goroutines, which is the behavior a shared client in an HTTP server needs.
One security detail worth calling out: the SDK blocks cross-origin redirects so an API key or a signed payment header cannot leak to another host. It is a small thing that many clients forget.
Streaming, the Go way
The Nebula Agent is stateless: your application owns the conversation and sends the full message history with each request. For a chat UI, StreamChatCompletion returns a ChatStream that behaves like a Go channel rather than a callback maze:
stream, err := client.Agent.StreamChatCompletion(ctx, request) if err != nil { return err } defer stream.Close() for chunk := range stream.Events() { for _, choice := range chunk.Choices { if choice.Delta != nil && choice.Delta.Content != nil { fmt.Print(*choice.Delta.Content) } } } if err := stream.Err(); err != nil { return err } Two details make this usable in real applications. First, a closed events channel does not mean the answer completed — you must check Err() afterward, because malformed events, API error frames, cancellation, and an unexpected end before [DONE] all surface there. Second, the configured timeout covers the entire stream, so a long answer needs a longer WithTimeout; the default 30 seconds is not enough for a thoughtful essay. Keep the defer stream.Close() even when you expect to drain the stream, since it also releases resources on an early return.
The Agent request itself is OpenAI-compatible, with roles for system, developer, user, and assistant, and an optional thinking mode for deeper answers. Billing is usage-based, with a 10-credit minimum in the pinned contract, so Usage is worth inspecting when it is present.
Forecasts and prediction markets
Two features deserve their own examples because they are where market intelligence stops being a dashboard and starts being a tool.
Forecasts are requested per asset, and a successful HTTP response can still say the asset is not forecastable. The SDK makes that state explicit instead of forcing you to guess from an empty array:
forecast, err := client.Forecasts.Mindshare(ctx, &nebula.MindshareForecastRequest{ Asset: &nebula.AssetRef{ID: "NVDA", Class: nebula.AssetClassEquity}, }) if err != nil { return err } if !forecast.IsForecastable() { if forecast.Reason != nil { fmt.Println("Forecast unavailable:", *forecast.Reason) } return nil } fmt.Println("Forecast points:", len(forecast.Series)) Both mindshare and sentiment forecasts expose Status, Reason, and IsForecastable(). Refresh: BoolPtr(true) asks for recomputation and can cost more credits; the pinned contract describes roughly five seconds for a refresh with a cached fallback after twenty, so the SDK's timeout defaults are worth revisiting if you enable it.
Prediction markets follow a similar "carry the identifier forward" pattern. Search with Predictions.ListMarkets, open the canonical market with Predictions.Market, then use the listing ID and outcome ID from that response to fetch history. The IDs refer to different objects, so each one has to come from its own field. The SDK keeps those types distinct, which prevents the classic bug of passing a market ID where an outcome ID belongs.
Pay per request with x402, carefully
Not every project wants an account and a credit balance. Nebula supports x402, an HTTP-native payment protocol, for eligible read operations: you pay per request in USDC on Base mainnet and skip the API key entirely. Thirty-seven of the 41 operations are eligible GETs; account details, the two forecast-generation endpoints, and Agent chat are not.
The x402 package is opt-in and deliberately paranoid. You can inspect an offer without a wallet or private key:
// Additional imports: "net/http" and "github.com/tigusigalpa/nebula-go/x402". offer, err := x402.PreviewOffer(ctx, nil, http.MethodGet, nebula.X402BaseURL+"/assets?query=NVIDIA") if err != nil { return err } fmt.Println("Resource:", offer.ResourceURL) fmt.Println("Network:", offer.Network, "recipient:", offer.PayTo) fmt.Println("Atomic USDC requested:", offer.Amount) Preview mode does not establish trust — it shows what the server is asking for. To actually pay, you supply your own signer and an explicit SpendPolicy with a per-payment cap and a nonempty recipient allowlist. The adapter checks the x402 version, the exact scheme, the Base network, the USDC asset, the recipient, the amount, and the resource URL before anything is signed. Production resources must use HTTPS, and signed GETs disable both SDK retries and the standard transport's implicit replay.
The README is refreshingly blunt about the risk: x402 can irreversibly spend real USDC, the cap applies per payment rather than in total, and a lost response after signing returns ErrPaymentUncertain, which you must reconcile before authorizing another payment. This is the right posture for a payment feature. It is also a good reason to start with API-key mode and add x402 only when pay-per-request genuinely fits your product.
Generated from a contract, not from memory
The most interesting engineering choice in this repository is not an API method. It is the pipeline behind the API methods.
The SDK is generated from a vendored OpenAPI 3.0.3 document pinned at info.version 1.0.2. go generate ./... rebuilds request structs, typed responses, enums, service methods, query validation rules, and x402 eligibility from that contract. The generator is offline: it does not download the spec or call Nebula, so regeneration works in CI and on a plane. A -check mode reports missing, edited, or obsolete generated files without writing anything, which makes drift a build failure instead of a slow surprise.
Naming choices that cannot be inferred from OpenAPI live in compat.json, and business rules that span several parameters live in handwritten request_validation.go. Generated code stays generated; human decisions stay human. That separation is why the project can claim a complete API surface without pretending every rule fits in a schema.
The test strategy follows the same principle. The suite runs without a Nebula key or a wallet, using local HTTP servers, an in-memory transport, and fixtures. A coverage guard ties all 41 operations to service calls, documentation, fixtures, and x402 eligibility. Contract tests exercise all 173 query parameters and all 750 response properties, including nullable fields, empty collections, large int64 values above 2^53, fractional timestamps, and unknown upstream fields retained in Raw. CI runs the race detector, go vet, staticcheck, govulncheck, CodeQL, formatting checks, and a generated-output check on both Go 1.24 and the current stable release.
That is a lot of ceremony for a young SDK, and it is the main reason to take the project seriously. Contract tests do not prove live API compatibility — the README says that too — but they do make silent regressions much less likely.
nebula-go vs. rolling your own HTTP client
Hand-rolling an API client is not wrong. Sometimes it is the right call: you need one endpoint, you want zero dependencies, or your organization has a standard HTTP stack. Here is an honest comparison.
| Concern | Hand-rolled net/http | nebula-go |
|---|---|---|
| Response modeling | map[string]any or partial structs | Typed structs for all 41 operations |
| Optional vs. zero values | Ambiguous without extra work | Pointers and helpers preserve intent |
| Retries | You implement and test them | Built in, with Retry-After support |
| Request validation | Discovered at runtime | Checked before the HTTP call |
| Streaming | Manual SSE parsing | ChatStream with Events() and Err() |
| Error handling | String matching or custom types | Sentinels plus rich APIError |
| Payments | Not applicable | Opt-in x402 adapter with spend policy |
| Dependencies | Whatever you choose | Standard library core |
| Maintenance | Yours | Upstream, with a pinned contract |
The trade-off is familiar: less code to own, more behavior to learn. For a service that talks to Nebula regularly, the SDK is the shorter path. For a one-off script, curl and encoding/json may still win.
Who is it for?
The SDK fits a few concrete shapes of work:
Research dashboards. Show sentiment, mindshare, and prediction-market odds next to price history without building an ingestion pipeline.
Monitoring and alerting jobs. Watch for momentum shifts or sentiment regime changes and notify a team when thresholds are crossed.
AI assistants and tool-using agents. Give an LLM typed access to market intelligence, with streamed answers when a human is reading.
Internal analyst tools. Let analysts search assets, compare communities, and pull forecasts without writing HTTP code.
Quant-adjacent research. Treat sentiment and attention as features alongside prices, with the usual caveat that correlation is not causation.
It is less suitable if you need a synchronous trading connection, ultra-low-latency market data, or an API surface that Nebula does not expose. This is an intelligence API, not an exchange feed.
FAQ
Is nebula-go free to use?
The SDK is MIT-licensed, so you can use, modify, and ship it freely. The Nebula API itself is a commercial service: account mode consumes credits, and x402 mode pays per request in USDC.
Which Go version do I need?
Go 1.24 or newer. That minimum comes from the module's dependency graph, including the optional x402 adapter.
Does it support streaming responses?
Yes. Agent.StreamChatCompletion returns a ChatStream backed by server-sent events. Read chunks from Events(), then check Err() to distinguish a completed answer from a truncated one.
Can I use it without an API key?
For the 37 x402-eligible GET operations, yes — with a wallet and a configured spend policy. Account details, forecast generation, and Agent chat require an API key.
How is it tested without hitting the live API?
The suite uses httptest servers, an in-memory transport, and saved fixtures. Contract tests compare the SDK against the pinned OpenAPI document, so go test ./... works offline and without a wallet.
Is it production-ready?
It is a v0.x project, so pin a version and review the changelog before upgrading. The test discipline, CI coverage, and release cadence are stronger than the version number suggests.
Wrapping up
nebula-go is a focused piece of engineering: one API, 41 operations, and a lot of attention to the parts of client code that usually get skipped. Typed responses make the data legible. The error taxonomy makes failures actionable. The retry policy respects the server. The generator keeps the SDK honest against its contract. And the x402 adapter treats payments with the caution they deserve.
If you are building anything in Go that touches market sentiment, prediction markets, or AI-generated market analysis, it is worth a look — and worth a star if you want the project to keep moving.
Links
Repository: github.com/tigusigalpa/nebula-go
Package docs: pkg.go.dev/github.com/tigusigalpa/nebula-go
Nebula API documentation: docs.hiddensystems.ai/nebula
API coverage matrix: docs/API-COVERAGE.md
Generation guide: docs/GENERATION.md
Compilable examples: example_test.go
Русская версия README: README.ru.md
Install it with go get github.com/tigusigalpa/nebula-go, set NEBULA_API_KEY, and ask your first question. The README's troubleshooting table covers the usual suspects — an empty key, a validation error, a plan window that is too short — and the issue tracker is the place for reproducible bugs.
Market data, sentiment, forecasts, and Agent output are not investment advice.