taphubserver

package module
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 14 Imported by: 0

README

taphub-sdk-go

Go SDK for the TapHub agency API with HMAC authentication. The Go counterpart of @taphubhq/sdk-server — same wire format, same error semantics, same retry behavior.

Server-only. This package signs every request with your agency secret. Never embed it in a binary distributed to untrusted hosts.

Installation

go get github.com/TaphubHQ/taphub-sdk-go@v0.5.0

Requires Go 1.22+.

Builder.GetBuilderLeaderboard requires backend support. The grid-api rank_leaderboardV2 query and the x-taphub-authorization user-forwarding header must be deployed server-side. Available from v0.5.0; see the operation below.

Auth.LoginS2S requires a multi-region backend. The mutation selects AuthResponse.homeRegion; a taphub-user-service whose schema predates multi-geo rejects the query and every login fails. See Multi-region.

v0.1.0 is retracted. The initial release shipped with a Wallet.Transfer wire body that the backend now rejects (missing tx_ref / currency, sends an obsolete direction field). Upgrade to v0.2.0+. See "Migrating from v0.1.0" below.

Configuration

All five string fields are required. The SDK does not read environment variables internally — you control how credentials are provided.

import (
    "context"
    "os"

    taphub "github.com/TaphubHQ/taphub-sdk-go"
)

client, err := taphub.New(taphub.Config{
    APIKey:       os.Getenv("TAPHUB_APIKEY"),
    AgencyID:     os.Getenv("TAPHUB_AGENCY_ID"),
    AgencySecret: os.Getenv("TAPHUB_AGENCY_SECRET"),
    BuilderCode:  os.Getenv("TAPHUB_BUILDER_CODE"),
    Endpoint:     os.Getenv("TAPHUB_ENDPOINT"), // e.g. https://api.taphub.io
})
if err != nil {
    // Missing field, or Endpoint not https:// / http://localhost.
    panic(err)
}

BuilderCode is the short identifier TapHub assigns each builder (agency). The backend resolves the canonical agency UUID from this code via the x-builder-code header. It is an identifier, not a secret, and is rendered verbatim by Config.String().

The Endpoint must use https:// (or http://localhost for local development). Plain http:// URLs are rejected.

Config.HTTPClient is optional. When nil, a default *http.Client with Timeout: 30 * time.Second is used. Pass your own to customize transport (proxies, TLS configuration, instrumentation, custom timeout).

Operations

client.User.Create

Provision a user under your agency.

raw, err := client.User.Create(ctx, taphub.UserCreateParams{
    AgencyUID: "user-123",
    Username:  "alice",
})
client.Wallet.Transfer

Transfer funds to or from a user's wallet. Ref is a mandatory idempotency key. Currency must match the agency configuration. Amount is always a positive decimal string — Direction controls the sign on the wire.

import "github.com/google/uuid" // any UUID library works

result, err := client.Wallet.Transfer(ctx, taphub.WalletTransferParams{
    Ref:       uuid.NewString(),
    AgencyUID: "user-123",
    Amount:    "10.00",
    Direction: taphub.DirectionDeposit,
    Currency:  "USDC.e",
})
// result: *taphub.WalletTransferResult{ID, WalletID, UserID, Amount, BeforeAmount, AfterAmount, Ref, Timestamp}

Direction must be DirectionDeposit or DirectionWithdraw. Amount is a string (decimal) to avoid floating-point precision loss on money values. The SDK rejects negative Amount inputs — use Direction to express withdraw instead.

client.Wallet.Reserve / Commit / Cancel — 2-phase reservation

For safe wrapping around irreversible external actions (on-chain withdraws, payment-gateway payouts, KYC holds), use the 2-phase API instead of Transfer. Reserve creates a PENDING wallet transaction and atomically moves funds from Wallet.Amount to Wallet.ReservedAmount. Follow with Commit (PENDING → FINISHED, funds finalized) on success, or Cancel (PENDING → FAILED, balance restored) on failure. The platform auto-cancels stale PENDING reservations after TTLSeconds (default 1800s, clamped server-side to [60, 86400]).

import "github.com/google/uuid"

ttl := 600 // optional; backend clamps to [60, 86400]
rsv, err := client.Wallet.Reserve(ctx, taphub.WalletReserveParams{
    Ref:        uuid.NewString(),
    AgencyUID:  "user-123",
    Amount:     "10.00",
    Direction:  taphub.DirectionWithdraw, // only Withdraw is currently accepted (see note)
    Currency:   "USDC.e",
    TTLSeconds: &ttl,
})
// rsv: *taphub.WalletReservation{ID, WalletID, UserID, Amount, BeforeAmount, AfterAmount, Ref, Status, TTLSeconds, ExpiresAt, Timestamp}

// ... perform on-chain tx, wait confirmation ...

if onChainConfirmed {
    _, err = client.Wallet.Commit(ctx, rsv.Ref)
} else {
    _, err = client.Wallet.Cancel(ctx, rsv.Ref)
}

Backend transport (since v0.4.0): GraphQL mutations agencyReserve / agencyCommit / agencyCancel on /th-user-gql. Same HMAC signing as LoginS2S. Error codes surface on *Error.Code via the GraphQL errors[].extensions.code field.

REST endpoints deprecated: the legacy REST paths POST /api/user/v1/agency/{reserve,commit,cancel} still respond but log a [deprecated] WARN per request server-side and will be removed in a future taphub-user-service release. Pin SDK ≥ v0.4.0 to ride the GraphQL path automatically.

Error code mappings (preserved on *Error.Code):

  • ErrTxRefConflict — replay with mismatched body, or another agency squatted the ref
  • ErrInsufficientBalance — Wallet.Amount < abs(Amount)
  • ErrReservationNotFound — unknown ref, or ref belongs to another agency
  • ErrReservationAlreadyCommitted — cancelling a FINISHED row
  • ErrReservationAlreadyCancelled — committing a FAILED row
  • ErrTransferModeNotSupported — agency is Seamless-mode

Direction restriction: the backend currently rejects deposit-direction reservations (positive amount) — see OpenSpec change user-260527-agency-wallet-reserve VULN-002. The SDK fails fast at the validation layer for DirectionDeposit. Only DirectionWithdraw is accepted until commit-of-deposit semantics are designed.

client.Auth

Server-to-server identity. Two methods:

client.Auth.LoginS2S

Exchange your agency's HMAC-signed identity for an access token keyed by a partner-supplied user ID. Use this from a builder backend.

resp, err := client.Auth.LoginS2S(ctx, taphub.LoginS2SInput{
    PartnerUserID: "user-123",
    DisplayName:   "alice",
})
// resp: *taphub.AuthResponse{AccessToken, User: AuthUser{ID, Name, Currency}, HomeRegion}

ClientMeta and Region are optional and are omitted from the request entirely when unset. If you run against more than one region, read Multi-region — there are obligations you cannot skip.

client.Auth.UserLogin

Exchange a Privy session token (issued to a builder frontend) for an access token. No HMAC — the session token is the credential.

resp, err := client.Auth.UserLogin(ctx, sessionToken)

GraphQL errors carrying extensions.code in {Unauthorized, TokenExpired, NotAuthenticated} surface as *Error{Kind: KindAuth}; all other GraphQL errors surface as *Error{Kind: KindValidation}. Backend code is preserved on *Error.Code. The three multi-region codes are classified separately — see Multi-region.

Wallet lookups

Three read-only methods. They share the HMAC transport with Transfer and return camelCase Go structs (no snake_case keys leak to callers).

client.Wallet.GetUserWallet
w, err := client.Wallet.GetUserWallet(ctx, "user-123")
// w: *taphub.Wallet{ID, Amount, Currency, IsEnabled}
client.Wallet.ListUserWallets
wallets, err := client.Wallet.ListUserWallets(ctx, "user-123")
// wallets: []taphub.Wallet
client.Wallet.GetTxByRef

Look up a wallet transaction by the same Ref passed to Transfer.

tx, err := client.Wallet.GetTxByRef(ctx, "my-ref-123")
// tx: *taphub.WalletTransaction
if errors.Is(err, taphub.ErrNotFound) {
    // Backend code preserved on the *Error, e.g. "TransactionNotFound".
}

v0.1 note: Lookup methods do not accept a currency parameter. The backend infers currency from your agency configuration.

client.Wallet.GetUserFundingSummary

Aggregated deposit/withdraw activity for one of your users, in one currency, over an optional window — for reconciling your funding ledger against TapHub's without paging the transaction list and summing it yourself.

sum, err := client.Wallet.GetUserFundingSummary(ctx, taphub.UserFundingSummaryParams{
    AgencyUID: "user-123",
    Currency:  "USDC.e",
    // From / To optional (*time.Time). Omit both for all-time.
})
// sum.DepositCount / sum.WithdrawCount        — finished transaction counts
// sum.DepositAmount / sum.WithdrawAmount      — decimal strings, both POSITIVE
// sum.PendingDepositCount, sum.PendingDepositAmount, … — same four, still in flight
// sum.From / sum.To                           — the window the server actually served

Currency is required: a user may hold several wallets, and a total summed across currencies is arithmetically valid but meaningless.

The window is half-open — From inclusive, To exclusive — so consecutive windows tile without double-counting a transaction on the boundary.

Withdrawals are returned as positive magnitudes, so DepositAmount - WithdrawAmount is the net funding flow. Amounts are decimal strings, never floats; pass them to shopspring/decimal if you need arithmetic.

Pending totals are reported separately rather than folded in, because that is where reconciliation mismatches live: a total short by 50 is a different problem depending on whether the 50 is in flight or lost.

Read this before drawing conclusions. These numbers cover only what TapHub recorded. A request that never reached TapHub — a network failure before the write — appears in none of them, so comparing balances alone cannot tell "a request was lost" apart from "the ledger is correct". Reconcile the count of requests you issued against DepositCount / WithdrawCount, not just the amounts.

Transfer-mode agencies only; seamless agencies are rejected with TransferModeNotSupported. An unknown AgencyUID returns an all-zero summary rather than an error — as does a uid belonging to another agency, deliberately, so the call cannot be used to probe another builder's user ids.

Requires a backend carrying the builderUserFundingSummary query (OpenSpec change builder-260820-funding-reconciliation); older deployments reject it as an unknown field.

client.Builder.GetBuilderLeaderboard (since v0.5.0)

Fetch the agency-scoped ("builder") leaderboard. Wraps grid-api's rank_leaderboardV2 query as an HMAC-signed server-to-server call. Period is required; all other fields are optional and fall back to server defaults (sort=pnl, sortDir=desc, limit=50, offset=0).

limit, offset := 50, 0
board, err := client.Builder.GetBuilderLeaderboard(ctx, taphub.BuilderLeaderboardInput{
    Period:  taphub.RankPeriodLast7d,        // today | yesterday | last_7d | last_30d | all_time | custom
    Sort:    taphub.RankSortPnl,             // pnl (net profit) | vol (volume); optional
    SortDir: taphub.SortDirDesc,             // asc | desc; optional
    Limit:   &limit, Offset: &offset,        // optional pagination
})
// board: *taphub.BuilderLeaderboard{ Total, Entries []RankEntry, Range, ... }
// Decimal fields (Pnl, Vol, Payout) are strings to preserve precision.
// The board is public + cacheable — it carries no per-user data. For the caller's
// own rank, use GetMyRank (below).

For RankPeriodCustom, supply an hour-aligned UTC window via Range. The bounds are time.Time values, sent to grid-api as ISO 8601 / RFC3339 (yyyy-mm-ddThh:mm:ssZ); the returned board.Range.From/To are the resolved (hour-aligned) time.Time window:

board, err := client.Builder.GetBuilderLeaderboard(ctx, taphub.BuilderLeaderboardInput{
    Period: taphub.RankPeriodCustom,
    Range: &taphub.TimeRange{
        From: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC),
        To:   time.Date(2024, 6, 2, 0, 0, 0, 0, time.UTC),
    },
})
client.Builder.GetMyRank

Fetch a specific user's own row + rank, the per-user companion to the public board (grid-api's rank_me query). The board and GetMyRank read the same ranked window, so the returned rank agrees with the board's ordering. Pass WithUserToken(accessToken) — the access token from Auth.LoginS2S / Auth.UserLogin; the user JWT is forwarded on the x-taphub-authorization header (the agency HMAC signature keeps Authorization). Without a user token there is no identity to rank: the method returns (nil, nil).

auth, _ := client.Auth.LoginS2S(ctx, taphub.LoginS2SInput{PartnerUserID: "user-123", DisplayName: "alice"})

mine, err := client.Builder.GetMyRank(ctx, taphub.MyRankInput{
    Period: taphub.RankPeriodLast7d,        // same window selector as the board, minus pagination
}, taphub.WithUserToken(auth.AccessToken))
// mine: *taphub.MyRank{ Rank, Total, Entry *RankEntry } for user-123 — even if outside the page.
// mine == nil when the caller is anonymous (no user token). mine.Entry == nil if no activity.

MyRankInput accepts the same RankPeriodCustom + Range as the board.

Transport: both methods target grid-api at /api/grid-api/grid-gql (the gateway's grid route), not /th-user-gql. Same HMAC signing as LoginS2S.

client.Builder.GetAgencyBids

List a page of full bid rows for the authenticated builder agency, plus the total match count. Wraps grid-api's agencyBids query as an HMAC-signed server-to-server call. The agency is resolved from the request context (x-builder-code + x-api-key); callers do not pass an agency ID.

res, err := client.Builder.GetAgencyBids(ctx, taphub.AgencyBidsInput{
    GameID:    "grid-ETH-USD", // optional
    AgencyUID: "buid-123",     // optional: the builder's own user id (reverse-mapped); exclusive with UserID
    UserID:    "user-123",     // optional grid-api user ID
    Status:    "pending",      // optional: pending | won | lost | cancelled
    Demo:      &demo,          // optional *bool: nil → no filter, true → demo bids only, false → real bids only
    Range:     &taphub.TimeRange{From: from, To: to}, // optional: filter by created_at (From <= To)
    Limit:     50,             // optional; server default 50, max 200
    Offset:    0,              // optional
})
// res: taphub.AgencyBidsResult{ Items []Bid, Total, Limit, Offset int }.
// Total is the full count matching the filter (independent of Limit/Offset).
// Each Bid carries AgencyUID + UserName (the user's LIVE display name). Decimal fields are
// strings, Time1/Time2 are unix seconds, CreatedAt/UpdatedAt/CancelledAt are RFC3339 strings.

Pass the zero-value AgencyBidsInput{} to use the backend defaults and list the agency's most recent bids. Sending both AgencyUID and UserID is rejected by grid-api (InvalidParam).

client.Builder.GetRetentionKpiCard (since v0.11.0)

The agency's retention KPI pair — the cohort of distinct users active in PreviousRange and the subset of that cohort that returned in CurrentRange. Wraps grid-api's builderRetentionKpiCard query as an HMAC-signed server-to-server call (x-builder-code + x-api-key + body HMAC, same auth as every Builder call); the agency is resolved server-side from those credentials — callers do not pass an agency ID.

card, err := client.Builder.GetRetentionKpiCard(ctx, taphub.RetentionKpiCardInput{
    ProductType:   taphub.RetentionProductPredictionGrid, // only prediction_grid is served today
    Granularity:   taphub.RetentionGranularityWeek,       // day | week | month
    PreviousRange: taphub.TimeRange{From: mon, To: mon.AddDate(0, 0, 7)}, // exactly ONE calendar bucket
    CurrentRange:  taphub.TimeRange{From: mon.AddDate(0, 0, 7), To: mon.AddDate(0, 0, 14)},
})
// card.CurrentRangeUsers is an INTERSECTION with the cohort (always <= PreviousRangeUsers) —
// never an independent count of the current window. Rate = current / previous (caller-side).

Each range must be exactly one UTC-aligned calendar bucket of Granularity (week = Monday 00:00 UTC + 7 days; month = the 1st + the real month length), non-overlapping and in order — grid-api rejects anything else with Retention_InvalidRange. Other rejections: Retention_ProductUnavailable (product line has no rollup data) and Retention_RangeBeyondRetention (range starts before the 800-day rollup horizon). All surface as *taphub.Error with Kind == KindValidation and the code in Error.Code.

client.Builder.GetRetentionTrend (since v0.11.0)

The agency's cohort-retention matrix — the latest N cohorts by granularity (day 14×7, week 12×8, month 12×12; fixed server-side), each followed across subsequent periods. Wraps grid-api's builderRetentionTrend query; same auth (x-builder-code + x-api-key + body HMAC) and error codes as GetRetentionKpiCard.

trend, err := client.Builder.GetRetentionTrend(ctx, taphub.RetentionTrendInput{
    ProductType: taphub.RetentionProductPredictionGrid,
    Granularity: taphub.RetentionGranularityWeek,
    // Anchor names ONE calendar period (normally the current one); the grid is derived
    // backwards from it (newest cohort = anchor − 1 period). A future anchor is rejected.
    Anchor: taphub.TimeRange{From: mon, To: mon.AddDate(0, 0, 7)},
})
// trend.Rows is newest-cohort-first, each with exactly trend.MaxPeriod cells.
// Cell contract (load-bearing): RetainedUsers == nil means the target period has NOT started;
// a pointer to 0 means "read, nobody returned"; the in-progress current period reports a
// partial, still-rising number. Never coalesce nil to 0. Compare a cell's TargetFrom against
// trend.DataThrough to identify the partial trailing diagonal.
client.Ping

Connectivity + auth check.

raw, err := client.Ping(ctx)

Multi-region

Backend requirement: the loginS2S mutation selects homeRegion, so this SDK requires a taphub-user-service on the multi-region schema. Against an older cluster every login fails with a GraphQL validation error. Verify before upgrading.

TapHub runs one full regional stack per region. Each user is pinned to exactly one home region, write-once at their FIRST login — their user row and wallet physically live there. The pin cannot be changed until the region-migration feature ships.

The regional endpoint that receives a user's first login becomes their home region, permanently. Route it wrong and every bid that user ever places pays a cross-continent round trip.

If you deploy against more than one region, three obligations bind (full contract: docs/sdk/multi-region-integration.md):

1. Persist homeRegion per user. Every login returns it. Reading it without storing it re-pins by luck on every login.

2. Route by the stored region. A user's subsequent loginS2S — and every other S2S call for them, wallet operations included — goes to that region's endpoint. The SDK does not route for you: it is a stateless client and home region is per-user state you own. Build one client per region and pick.

3. Retry once on Auth_WrongRegion.

resp, err := clientFor(storedRegion).Auth.LoginS2S(ctx, in)
if region, ok := taphub.WrongRegionTarget(err); ok {
    resp, err = clientFor(region).Auth.LoginS2S(ctx, in) // ONCE — never a loop
    // ...then persist `region` so the next login routes straight there.
}

Pinning is write-once, so a second rejection means a bug, not a race. The SDK deliberately does not retry for you: it would fire before you could persist the correction, and it would hide a cross-continent hop inside a call that looks local.

First-join routing

For a user with no stored region, route their first login to the region nearest them. If your frontend measures which region responds fastest, forward its proposal in ClientMeta under preferredRegion — the backend validates it against its deployed regions and uses it to pin.

meta := `{"tz":"Asia/Ho_Chi_Minh","preferredRegion":"sg"}` // built by your FE, forwarded verbatim
resp, err := client.Auth.LoginS2S(ctx, taphub.LoginS2SInput{
    PartnerUserID: "user-123",
    DisplayName:   "alice",
    ClientMeta:    &meta,
})

ClientMeta is sent byte for byte — the SDK never parses or re-encodes it. Alternatively, Region names the caller's own current region as a first-join candidate; leave it empty if you geo-route instead.

HomeRegion is empty when the backend returns null: the region-directory is unconfigured (feature off), or this is a demo/refresh path.

Region error codes
Code Constant Kind Retried by the SDK Your reaction
Auth_WrongRegion CodeAuthWrongRegion KindAuth no Retry once at WrongRegionTarget(err); update the stored region
Auth_RegionUnavailable CodeAuthRegionUnavailable KindInternal yes Transient directory outage — the SDK's backoff already handles it
Auth_AgencyConfigUnavailable CodeAuthAgencyConfigUnavailable KindAuth no Usually a seeding gap on the TapHub side; contact TapHub if it persists

Auth_RegionUnavailable is the one Auth_* code deliberately not classified as KindAuth — that would suppress the retry the contract asks for and turn a momentary directory blip into a failed login.

Wallet operations land region-local

A user's wallet row lives in their home region. A wallet request for a user pinned elsewhere returns user-not-found and is quarantined on the TapHub side for manual review — it is never silently applied to a wrong-region wallet. Route wallet calls with the same stored homeRegion you use for logins.

Migrating from v0.1.0

v0.2.0 is a breaking minor bump (pre-1.0 SemVer rule). Three changes:

  1. Config requires BuilderCode. The TapHub team assigns each builder a short code; you receive it alongside your APIKey / AgencyID. Without it the SDK refuses to construct and the backend returns 401 because its routing middleware reads x-builder-code.

    client, _ := taphub.New(taphub.Config{
        APIKey:       "...",
        AgencyID:     "...",
        AgencySecret: "...",
        BuilderCode:  "acme-corp", // NEW
        Endpoint:     "...",
    })
    
  2. Wallet.Transfer adds required Currency and returns a typed *WalletTransferResult.

    v0.1.0:

    raw, _ := client.Wallet.Transfer(ctx, taphub.WalletTransferParams{
        Ref: "r", AgencyUID: "u", Amount: "10", Direction: taphub.DirectionDeposit,
    })
    // raw: json.RawMessage
    

    v0.2.0:

    result, _ := client.Wallet.Transfer(ctx, taphub.WalletTransferParams{
        Ref: "r", AgencyUID: "u", Amount: "10", Direction: taphub.DirectionDeposit,
        Currency: "USDC.e", // NEW required
    })
    // result: *taphub.WalletTransferResult{...}
    

    The wire body also changed: ref → tx_ref, direction removed, amount is sign-encoded for withdraw. All handled transparently by the SDK; you do not touch the wire.

  3. Auth module is new. Existing callers do not have to use it, but if you previously rolled your own GraphQL client to call userLogin / loginS2S, you can replace that with client.Auth.UserLogin(ctx, token) or client.Auth.LoginS2S(ctx, input).

The compiler points at every site that needs updating: missing BuilderCode on Config, missing Currency on WalletTransferParams, and the changed Wallet.Transfer return type.

Security

Do not intercept the request body

The SDK signs the request body with HMAC-SHA256. The same bytes that are signed are sent on the wire. If your *http.Client transport, a proxy, or any middleware modifies, re-marshals, or recompresses the JSON, the signature will not match and the server will reject the request with an authentication error.

If you wrap Config.HTTPClient.Transport, do not touch req.Body.

Secret redaction

Config.AgencySecret never appears in fmt.Sprintf("%v", cfg), in json.Marshal(cfg), or in any field of the returned *Error. The transport never logs signatures.

Replay risk (v0.1)

In v0.1, each signed request is valid indefinitely if replayed. A future version will add a timestamp + nonce in the HMAC payload, coordinated with a backend change. In the meantime:

  • Use Ref-based idempotency for Wallet.Transfer (the backend dedupes by Ref).
  • Use distinct AgencyUID values for User.Create (the backend dedupes by agency_uid).
  • Ensure your agency's IP whitelist is configured in the TapHub dashboard.

Error handling

Every operation returns either (result, nil) or (nil, *taphub.Error). The *Error value carries:

type Error struct {
    Kind       ErrorKind        // one of KindValidation, KindAuth, KindNotFound, ...
    Code       string           // backend error tag or "HTTP_<status>"
    Status     int              // HTTP status, 0 for network failures
    Message    string
    RetryAfter time.Duration    // from Retry-After header, when present
    Cause      error            // underlying transport/context error
    Details    json.RawMessage  // raw JSON error body, when present
    Meta       map[string]any   // errors[0].extensions.meta, when present
}

Meta carries the backend's structured error data. Today its only consumer is multi-region: Auth_WrongRegion puts the correct region under Meta["homeRegion"] — read it with WrongRegionTarget rather than indexing by hand.

Branch on Kind using the exported sentinels with errors.Is:

if _, err := client.Ping(ctx); err != nil {
    switch {
    case errors.Is(err, taphub.ErrAuth):
        // 401, 403 — wrong signature, blocked IP, etc.
    case errors.Is(err, taphub.ErrRateLimit):
        // 429 — back off.
    case errors.Is(err, taphub.ErrNetwork):
        // DNS / TCP / TLS / context cancellation.
    }
}

Inspect details with errors.As:

var e *taphub.Error
if errors.As(err, &e) && e.Code == "RefAlreadyUsed" {
    // Idempotent duplicate — safe to treat as already-applied.
}
Kind HTTP status
KindValidation 400, 422
KindAuth 401, 403
KindNotFound 404
KindConflict 409
KindRateLimit 429
KindInternal 500–599, unmapped non-2xx
KindNetwork DNS/TCP/TLS failures, context cancellation

Retry

The SDK retries transient failures with full-jitter exponential backoff.

Defaults
Field Default Description
MaxAttempts 3 Total attempts, including the first
BaseDelay 200ms Base for exponential backoff
MaxDelay 5s Upper bound per sleep
Trigger matrix
Kind Retry?
KindNetwork yes
KindInternal (5xx) yes
KindRateLimit (429) yes
KindAuth no
KindValidation no
KindNotFound no
KindConflict no

Synchronous validation errors (rejected before any HTTP request) bypass the retry loop entirely.

Backoff formula
cap   = min(MaxDelay, BaseDelay * 2^attemptIndex)
delay = rand.Float64() * cap

attemptIndex is 0-based for the first scheduled retry.

Retry-After honoring

If a failed response carries a Retry-After header (integer seconds or HTTP-date), the SDK uses that value (clamped to MaxDelay) for the next attempt. No jitter is applied to a server-supplied delay.

Disable per call
_, err := client.Wallet.Transfer(
    ctx,
    taphub.WalletTransferParams{Ref: "r-1", AgencyUID: "u-1", Amount: "1.00", Direction: taphub.DirectionDeposit},
    taphub.WithRetryDisabled(),
)
Disable for the whole client
client, _ := taphub.New(taphub.Config{
    APIKey: ..., AgencyID: ..., AgencySecret: ..., Endpoint: ...,
    Retry: &taphub.RetryConfig{Disabled: true},
})
Observability with OnRetry
client, _ := taphub.New(taphub.Config{
    APIKey: ..., AgencyID: ..., AgencySecret: ..., Endpoint: ...,
    Retry: &taphub.RetryConfig{
        OnRetry: func(info taphub.RetryInfo) {
            log.Printf("retry attempt=%d kind=%s sleep=%s",
                info.Attempt, info.Err.Kind, info.NextDelay)
        },
    },
})

A panic inside OnRetry is recovered and discarded — the retry loop never crashes the calling goroutine.

Context cancellation

If the request context.Context is cancelled or its deadline expires during a retry sleep, the loop exits immediately. The returned error has Kind == KindNetwork with Cause set to context.Canceled or context.DeadlineExceeded.

Mutation retry safety

Retry applies uniformly to mutations and reads. The backend dedupes by idempotency keys:

  • Wallet.Transfer dedupes by Ref.
  • User.Create dedupes by AgencyUID.

A duplicate surfaces as *Error with Kind == KindConflict (e.g. Code == "RefAlreadyUsed").

HMAC algorithm

The SDK uses HMAC-SHA256 over the raw UTF-8 bytes of the JSON request body, hex-encoded as a 64-character lowercase string. This matches the algorithm used by the backend verifier and by the TypeScript SDK (crypto.createHmac('sha256', secret).update(body).digest('hex')):

// Equivalent of the backend's hmac.GenerateHMAC and the TS createHmac call.
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
sig := hex.EncodeToString(h.Sum(nil))

A pinned (secret, body, expectedHex) fixture lives in testdata/hmac_vector.json and is verified by Test_HMAC_PinnedVector. The same fixture is reproducible by the Go reference and by the TS SDK; that is the cross-language wire-compat anchor.

Testing

Unit tests (no network):

go test ./...

Integration tests (require running dev-stack.sh and exported env vars):

export TAPHUB_APIKEY=...
export TAPHUB_AGENCY_ID=...
export TAPHUB_AGENCY_SECRET=...
export TAPHUB_ENDPOINT=https://api.taphub.io
go test -tags=integration ./...

Integration tests skip cleanly when any of those variables is unset.

License

MIT.

Documentation

Overview

Package taphubserver is the Go SDK for the TapHub agency API.

It mirrors the @taphubhq/sdk-server TypeScript package in wire format, error taxonomy, and retry semantics. Server-only; do not import from client-side code that ships an HMAC secret to untrusted runtimes.

Index

Constants

View Source
const (
	// CodeAuthWrongRegion means the user is pinned to a DIFFERENT region than
	// the endpoint that was called. Meta["homeRegion"] names the right one:
	// retry the login ONCE there (never a loop — pinning is write-once, so a
	// second rejection means a real bug, not a race) and persist the corrected
	// region so the next login routes straight there.
	CodeAuthWrongRegion = "Auth_WrongRegion"

	// CodeAuthRegionUnavailable means the global region-directory is
	// unreachable and the backend failed closed. Transient: classified
	// KindInternal so the SDK's own backoff retries it.
	CodeAuthRegionUnavailable = "Auth_RegionUnavailable"

	// CodeAuthAgencyConfigUnavailable means the agency's config could not be
	// sourced in that region — usually a seeding gap on the TapHub side.
	// Retrying cannot fix it; contact TapHub if it persists.
	CodeAuthAgencyConfigUnavailable = "Auth_AgencyConfigUnavailable"
)

Multi-region error codes (extensions.code) from taphub-user-service's login path. See docs/sdk/multi-region-integration.md for the builder obligations each one implies.

Variables

View Source
var (
	ErrValidation = &Error{Kind: KindValidation}
	ErrAuth       = &Error{Kind: KindAuth}
	ErrNotFound   = &Error{Kind: KindNotFound}
	ErrConflict   = &Error{Kind: KindConflict}
	ErrRateLimit  = &Error{Kind: KindRateLimit}
	ErrInternal   = &Error{Kind: KindInternal}
	ErrNetwork    = &Error{Kind: KindNetwork}
)

Sentinel values for use with errors.Is. The fields other than Kind are intentionally zero; comparing by Kind is the documented predicate.

Functions

func WrongRegionTarget added in v0.11.0

func WrongRegionTarget(err error) (string, bool)

WrongRegionTarget reports the region a login must be retried at after an Auth_WrongRegion rejection, implementing obligation 3 of the multi-region contract:

resp, err := client.Auth.LoginS2S(ctx, in)
if region, ok := taphubserver.WrongRegionTarget(err); ok {
    resp, err = clientFor(region).Auth.LoginS2S(ctx, in) // ONCE, not a loop
}

Returns ok=false for a nil error, a non-*Error, any other code, or an Auth_WrongRegion whose meta carries no usable region — a caller that cannot learn the target must surface the original error rather than guess.

Types

type AgencyBidsInput added in v0.11.0

type AgencyBidsInput struct {
	GameID    string
	AgencyUID string
	UserID    string
	Status    string
	Demo      *bool      // nil → no demo filter; true → demo bids only; false → real bids only
	Range     *TimeRange // optional created_at filter; both bounds required, From<=To (validated)
	Limit     int
	Offset    int
}

AgencyBidsInput drives GetAgencyBids. All fields are optional: a zero-value input asks grid-api for the authenticated agency's default bid list. GameID accepts grid-api's gameId filter; Status is the backend string filter (pending, won, lost, cancelled). AgencyUID filters to one builder user by the builder's own user id (reverse-mapped server-side); it is mutually exclusive with UserID. From/To filter by bid placement time (created_at) and must be supplied TOGETHER (both or neither), with From <= To — GetAgencyBids validates this before the call. Demo filters by the bid's demo flag: nil → no filter (mixed list), true → demo bids only, false → real bids only.

type AgencyBidsResult added in v0.11.0

type AgencyBidsResult struct {
	Items  []Bid
	Total  int
	Limit  int
	Offset int
}

AgencyBidsResult is one page of agency bids. Items is the current page (created_at DESC); Total is the full count matching the filter (independent of Limit/Offset); Limit/Offset are the applied (clamped) paging values.

type AuthModule added in v0.2.0

type AuthModule struct {
	// contains filtered or unexported fields
}

AuthModule groups identity operations under Client.Auth.

func (*AuthModule) LoginS2S added in v0.2.0

func (m *AuthModule) LoginS2S(
	ctx context.Context,
	input LoginS2SInput,
	opts ...CallOption,
) (*AuthResponse, error)

LoginS2S is the server-to-server login. The caller authenticates via the agency HMAC secret (signed over the GraphQL request body) and receives an access token keyed by the supplied partnerUserId. apikey is threaded into the input variables so the backend resolver picks up the same value that signed the request — callers do not pass it directly.

func (*AuthModule) UserLogin added in v0.2.0

func (m *AuthModule) UserLogin(
	ctx context.Context,
	sessionToken string,
	opts ...CallOption,
) (*AuthResponse, error)

UserLogin exchanges a Privy session token (issued to a builder frontend) for an agency-scoped access token. Plain GraphQL: no HMAC, because the caller has nothing else to prove identity with at this point.

type AuthResponse added in v0.2.0

type AuthResponse struct {
	AccessToken string   `json:"accessToken"`
	User        AuthUser `json:"user"`

	// HomeRegion is the region this user is PINNED to — where their user row
	// and wallet physically live (multi-geo, docs/sdk/multi-region-integration.md).
	// Empty when the backend returns null: the region-directory is unconfigured
	// (feature off) or this is a demo/refresh path.
	//
	// A builder that runs against more than one region MUST persist this per
	// user and route that user's subsequent S2S calls to the matching
	// endpoint. Reading it without persisting it re-pins by luck on every
	// login.
	HomeRegion string `json:"homeRegion,omitempty"`
}

AuthResponse is the result of UserLogin / LoginS2S: a bearer access token and the user it represents. Callers attach AccessToken to subsequent downstream calls (out of SDK scope).

The struct is value-comparable; keep it that way (no pointer or map fields) so callers can assert on it with ==.

type AuthUser added in v0.2.0

type AuthUser struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	Currency string `json:"currency"`
}

AuthUser is the lightweight user shape returned alongside an access token. Matches the TS SDK's AuthUser. The struct tolerates unknown fields so a backend addition does not break consumers.

type Bid added in v0.9.0

type Bid struct {
	ID           string
	Ref          string // wire tx_ref
	AgencyID     string
	UserID       string
	PairID       string
	Currency     string
	Amount       string
	Coefficient  string
	Time1        int64
	Time2        int64
	Price1       string
	Price2       string
	Status       string
	Charged      *bool
	ChargedTxID  *string
	Win          *bool
	Rewarded     *bool
	Payout       *string
	RewardTxID   *string
	Cancelled    *bool
	CancelledAt  *string
	RefundRate   *string
	RefundAmount *string
	Demo         bool
	Slippage     int
	CreatedAt    string
	UpdatedAt    *string
	Meta         *string
	RelativeLoc  *string
	// AgencyUID + UserName are populated ONLY by GetAgencyBids (agencyBids) — the builder's
	// own user id and the user's LIVE display name. Nil for the per-user bid reads.
	AgencyUID *string
	UserName  *string
}

Bid is a single bid as returned by GetBuilderUserOpenBids and GetBuilderUserClosedBids. Decimal fields are strings; Time1/Time2 are unix seconds; CreatedAt/UpdatedAt/CancelledAt are RFC3339. Closed bids populate outcome fields (Win, Rewarded, Payout, Cancelled, etc.); open bids leave them at their zero value.

type BuilderCurrencyBalance added in v0.11.0

type BuilderCurrencyBalance struct {
	Currency string `json:"currency"`
	// TotalNow is what the agency's users hold right now, INCLUDING funds
	// reserved for in-flight bids. It is "what users hold", not "what they can
	// spend this second": money held against an open bid is still theirs.
	TotalNow string `json:"totalNow"`
	// TotalAsOf is the same total as it stood at AsOfTs. Because both figures
	// treat reserved funds identically, TotalNow - TotalAsOf reflects real money
	// movement rather than how many bids happen to be open at call time.
	TotalAsOf string `json:"totalAsOf"`
	// WalletCount is how many wallets contribute to TotalNow.
	WalletCount int `json:"walletCount"`
}

BuilderCurrencyBalance is one currency's slice of the agency's user balance. The Decimal fields are strings to preserve precision — parse them with a decimal library, never into float64.

type BuilderLeaderboard added in v0.5.0

type BuilderLeaderboard struct {
	Period  RankPeriod    `json:"period"`
	Range   RankTimeRange `json:"range"`
	Sort    RankSort      `json:"sort"`
	SortDir SortDir       `json:"sortDir"`
	Total   int           `json:"total"`
	Entries []RankEntry   `json:"entries"`
}

BuilderLeaderboard is the result of GetBuilderLeaderboard. It is the public board: identical for every caller of the agency for a given (period, sort, sortDir, page), so it carries no per-user data. The caller's own row+rank is fetched separately via GetMyRank (bid-260606-leaderboard-split-my-rank).

type BuilderLeaderboardInput added in v0.5.0

type BuilderLeaderboardInput struct {
	Period  RankPeriod
	Range   *TimeRange // required iff Period == RankPeriodCustom
	Sort    RankSort   // "" → server default (pnl)
	SortDir SortDir    // "" → server default (desc)
	Limit   *int       // nil → server default (50)
	Offset  *int       // nil → server default (0)
}

BuilderLeaderboardInput drives GetBuilderLeaderboard. Period is the only required field; every other field is optional and, when left zero/nil, falls back to grid-api's defaults (sort=pnl, sortDir=desc, limit=50, offset=0, max limit clamped to 500 server-side).

type BuilderModule added in v0.5.0

type BuilderModule struct {
	// contains filtered or unexported fields
}

BuilderModule groups agency-scoped ("builder") operations under Client.Builder. These are server-to-server reads/writes that act on the agency identified by the configured BuilderCode — no end-user identity is involved. The leaderboard read targets grid-api (not user-service).

Added in v0.5.0.

func (*BuilderModule) GetAgencyBids added in v0.11.0

func (m *BuilderModule) GetAgencyBids(
	ctx context.Context,
	input AgencyBidsInput,
	opts ...CallOption,
) (AgencyBidsResult, error)

GetAgencyBids lists one page of full bid rows for the authenticated S2S builder agency from grid-api's agencyBids query, plus the total match count. The agency is resolved by grid-api from the HMAC-signed builder context, not from a caller-supplied GraphQL argument.

func (*BuilderModule) GetBuilderLeaderboard added in v0.5.0

func (m *BuilderModule) GetBuilderLeaderboard(
	ctx context.Context,
	input BuilderLeaderboardInput,
	opts ...CallOption,
) (*BuilderLeaderboard, error)

GetBuilderLeaderboard fetches the agency-scoped public leaderboard from grid-api's rank_leaderboardV2 query. It is an HMAC-signed server-to-server call: grid-api's builder-auth middleware classifies x-api-key + x-builder-code as S2S and verifies the HMAC signature when the agency has a secret. The agency is resolved from the configured BuilderCode.

The board is public and cacheable — it carries no per-user data. To resolve a specific user's own row + rank, use GetMyRank with WithUserToken(accessToken).

func (*BuilderModule) GetBuilderStats added in v0.9.0

func (m *BuilderModule) GetBuilderStats(
	ctx context.Context,
	input BuilderStatsInput,
	opts ...CallOption,
) (*BuilderStats, error)

GetBuilderStats fetches the agency's gameplay metrics (trading volume, revenue, active users) from grid-api's builderStats query over the given window. HMAC-signed S2S call; the agency is resolved from the configured BuilderCode. Sealed-hours-only — see BuilderStats.AsOf.

func (*BuilderModule) GetBuilderUserBalances added in v0.11.0

func (m *BuilderModule) GetBuilderUserBalances(
	ctx context.Context,
	input BuilderUserBalancesInput,
	opts ...CallOption,
) (*BuilderUserBalances, error)

GetBuilderUserBalances returns the total balance held by the calling agency's users, now and at a past UTC day boundary, broken down by currency.

Server-to-server only: the agency is derived from the configured BuilderCode and HMAC credentials, never from an argument, so a builder can only read its own users. Targets user-service (not grid-api).

An agency with no wallets returns an empty Balances slice — not an error and not a zero row. Render that as an empty state so a misconfigured integration stays distinguishable from a genuinely empty one.

Added in v0.11.0.

func (*BuilderModule) GetBuilderUserClosedBids added in v0.9.0

func (m *BuilderModule) GetBuilderUserClosedBids(
	ctx context.Context,
	input BuilderUserClosedBidsInput,
	opts ...CallOption,
) ([]Bid, error)

GetBuilderUserClosedBids lists one user's CLOSED bids (won + lost + cancelled — every bid that is NOT still open) by agencyUid, from grid-api's builderUserClosedBids query. It is the mirror of GetBuilderUserOpenBids: same HMAC-signed S2S call to grid-api, same agencyUid→userId reverse-map performed server-side; only the returned status set differs (the complement of open).

func (*BuilderModule) GetBuilderUserKpis added in v0.11.0

func (m *BuilderModule) GetBuilderUserKpis(
	ctx context.Context,
	input BuilderUserKpisInput,
	opts ...CallOption,
) (*BuilderUserKpis, error)

GetBuilderUserKpis fetches the S2S-only aggregate KPI summary for the configured builder from grid-api's builderUserKpis query. The agency is resolved from the configured BuilderCode and API key; it is never passed as an argument.

func (*BuilderModule) GetBuilderUserOpenBids added in v0.9.0

func (m *BuilderModule) GetBuilderUserOpenBids(
	ctx context.Context,
	input BuilderUserOpenBidsInput,
	opts ...CallOption,
) ([]Bid, error)

GetBuilderUserOpenBids lists one user's OPEN (pending) bids by agencyUid, from grid-api's builderUserOpenBids query. grid-api reverse-maps agencyUid→userId internally; this targets the grid-api endpoint (not user-service).

func (*BuilderModule) GetBuilderUserTransactions added in v0.9.0

func (m *BuilderModule) GetBuilderUserTransactions(
	ctx context.Context,
	input BuilderUserTransactionsInput,
	opts ...CallOption,
) ([]WalletTransaction, error)

GetBuilderUserTransactions lists one user's wallet transactions by agencyUid, from taphub-user-service's builderUserTransactions query.

func (*BuilderModule) GetBuilderUsersStats added in v0.7.0

func (m *BuilderModule) GetBuilderUsersStats(
	ctx context.Context,
	input BuilderUsersStatsInput,
	opts ...CallOption,
) ([]RankEntry, error)

GetBuilderUsersStats fetches per-user PnL / volume for a specific set of the builder's own users (by agency_uid) over a period — `rank_me` generalized to a batch. It is an HMAC-signed S2S call (same auth as GetBuilderLeaderboard); the agency is resolved from the configured BuilderCode, never passed as an argument.

The response contains one RankEntry per requested agency_uid, in input order. Each row's Rank is the user's competition rank in the FULL agency pool for the window (so it agrees with GetBuilderLeaderboard / GetMyRank). An agency_uid with no activity in the window comes back as a zero row that echoes only AgencyUID. Added in v0.7.0 (bid-260609).

func (*BuilderModule) GetBuilderWalletStats added in v0.9.0

func (m *BuilderModule) GetBuilderWalletStats(
	ctx context.Context,
	input BuilderWalletStatsInput,
	opts ...CallOption,
) (*BuilderWalletStats, error)

GetBuilderWalletStats fetches the agency's wallet/registration metrics (new users, deposit and withdraw volume) from taphub-user-service's builderWalletStats query over the given window. HMAC-signed S2S call; the agency is resolved from the configured BuilderCode. Note this targets the user-service endpoint (not grid-api) and its window uses unix-second bounds on the wire — the SDK converts the Range for you.

func (*BuilderModule) GetMyRank added in v0.6.0

func (m *BuilderModule) GetMyRank(
	ctx context.Context,
	input MyRankInput,
	opts ...CallOption,
) (*MyRank, error)

GetMyRank fetches the caller's own leaderboard row + rank from grid-api's rank_me query — the per-user companion to the public GetBuilderLeaderboard board, reading the same ranked window so the rank agrees with the board's ordering.

The caller's identity MUST be forwarded with WithUserToken(accessToken); grid-api resolves the user from that JWT (x-taphub-authorization). Without a user token there is no identity to rank: grid-api returns null and this method returns (nil, nil).

func (*BuilderModule) GetRetentionKpiCard added in v0.11.0

func (m *BuilderModule) GetRetentionKpiCard(
	ctx context.Context,
	input RetentionKpiCardInput,
	opts ...CallOption,
) (*RetentionKpiCard, error)

GetRetentionKpiCard fetches the agency's retention pair — the size of the cohort active in PreviousRange and the subset of that cohort that returned in CurrentRange — from grid-api's builderRetentionKpiCard query. HMAC-signed S2S call; the agency is resolved from the configured BuilderCode. "Active" = >=1 booked (settled OR cancelled) bid, the same user set behind builderAgencyStats.users, so the two dashboard cards agree for the same window.

func (*BuilderModule) GetRetentionTrend added in v0.11.0

func (m *BuilderModule) GetRetentionTrend(
	ctx context.Context,
	input RetentionTrendInput,
	opts ...CallOption,
) (*RetentionTrend, error)

GetRetentionTrend fetches the agency's cohort-retention matrix (day 14×7, week 12×8, month 12×12 — fixed server-side so every consumer draws the same chart) from grid-api's builderRetentionTrend query. HMAC-signed S2S call; the agency is resolved from the configured BuilderCode. See RetentionPeriod for the nil-vs-0 contract on cells.

type BuilderStats added in v0.9.0

type BuilderStats struct {
	// Range is the resolved (hour-aligned) window the server applied.
	Range RankTimeRange `json:"range"`
	// AsOf is the latest sealed-hour boundary included — the metrics are "as of" this instant
	// (the open hour is excluded), so the UI can show freshness.
	AsOf          time.Time `json:"asOf"`
	TradingVolume string    `json:"tradingVolume"` // Σ vol (total wagered, all pairs)
	Revenue       string    `json:"revenue"`       // −Σ pnl (house net; may be negative)
	ActiveUsers   int       `json:"activeUsers"`   // distinct non-bot users active in the window
}

BuilderStats is the result of GetBuilderStats — grid-api gameplay metrics for the agency, derived from the leaderboard rollups (sealed hours only). Decimal fields are strings to preserve precision; Revenue is the house net P&L (−Σ pnl) and MAY be negative when players net-won over the window.

type BuilderStatsInput added in v0.9.0

type BuilderStatsInput struct {
	Period RankPeriod
	Range  *TimeRange
}

BuilderStatsInput selects the window for GetBuilderStats. Period is required; Range is required iff Period == RankPeriodCustom (hour-aligned UTC bounds), ignored otherwise.

type BuilderUserBalances added in v0.11.0

type BuilderUserBalances struct {
	// NowTs is the server instant at which TotalNow was measured (unix seconds).
	NowTs int64 `json:"nowTs"`
	// AsOfTs is the resolved comparison instant: 00:00 UTC, DaysAgo days back
	// (unix seconds). Read this rather than recomputing it — it derives from the
	// server clock, so a locally computed cutoff will drift from it.
	AsOfTs   int64                    `json:"asOfTs"`
	Balances []BuilderCurrencyBalance `json:"balances"`
}

BuilderUserBalances is the result of GetBuilderUserBalances: what the calling agency's users hold now versus at a past UTC day boundary, per currency.

Balances are never summed across currencies — this service owns no exchange rate, so adding two currencies would produce a meaningless number. Convert with a rate you own if you need a single figure, and label it as converted.

Added in v0.11.0.

type BuilderUserBalancesInput added in v0.11.0

type BuilderUserBalancesInput struct {
	// DaysAgo is how many whole UTC days back to compare against. nil → server
	// default (7). Must be 1..90 when set.
	//
	// Days rather than a timestamp because balances are recorded once per day at
	// 00:00 UTC — there is no sub-day precision the server could honour, and an
	// instant argument would only invite callers to supply precision it discards.
	DaysAgo *int
	// Currency narrows the result to one currency. "" → every currency the
	// agency's users hold.
	Currency string
}

BuilderUserBalancesInput selects the comparison point for GetBuilderUserBalances. Both fields are optional: the zero value asks for "7 days ago, every currency".

type BuilderUserClosedBidsInput added in v0.9.0

type BuilderUserClosedBidsInput struct {
	AgencyUID string
	PairID    string
	Limit     int
	Offset    int
}

BuilderUserClosedBidsInput drives GetBuilderUserClosedBids. Same shape as BuilderUserOpenBidsInput: AgencyUID is required; PairID, Limit and Offset are optional.

type BuilderUserKpis added in v0.11.0

type BuilderUserKpis struct {
	Period             RankPeriod    `json:"period"`
	Range              RankTimeRange `json:"range"`
	NumberOfUserTrade  int           `json:"numberOfUserTrade"`
	NewUser            int           `json:"newUser"`
	TotalTradingVolume string        `json:"totalTradingVolume"`
	UpdatedAt          time.Time     `json:"updatedAt"`
}

BuilderUserKpis is an agency-scoped aggregate KPI summary for the configured builder. Decimal fields are serialized as strings to preserve precision.

type BuilderUserKpisInput added in v0.11.0

type BuilderUserKpisInput struct {
	Period RankPeriod
	Range  *TimeRange // required iff Period == RankPeriodCustom
}

BuilderUserKpisInput selects the window for GetBuilderUserKpis. Period is required by the SDK for explicitness; use RankPeriodAllTime for the backend's default window. Range is required iff Period == RankPeriodCustom.

type BuilderUserOpenBidsInput added in v0.9.0

type BuilderUserOpenBidsInput struct {
	AgencyUID string
	PairID    string
	Limit     int
	Offset    int
}

BuilderUserOpenBidsInput drives GetBuilderUserOpenBids. AgencyUID is required; PairID, Limit and Offset are optional.

type BuilderUserTransactionsInput added in v0.9.0

type BuilderUserTransactionsInput struct {
	AgencyUID string
	Limit     int // 0 → server default
	Offset    int
}

BuilderUserTransactionsInput drives GetBuilderUserTransactions. AgencyUID is required; Limit and Offset are optional (Limit is clamped server-side to ≤50).

type BuilderUsersStatsInput added in v0.7.0

type BuilderUsersStatsInput struct {
	AgencyUIDs []string
	Period     RankPeriod
	Range      *TimeRange // required iff Period == RankPeriodCustom
	Sort       RankSort   // "" → server default (pnl)
	SortDir    SortDir    // "" → server default (desc)
}

BuilderUsersStatsInput drives GetBuilderUsersStats. AgencyUIDs (the builder's own user ids) and Period are required; Range is required iff Period == RankPeriodCustom. Sort/ SortDir only choose which board's `rank` is reported (the metrics are sort-independent); left zero, they fall back to grid-api's defaults (pnl/desc).

type BuilderWalletStats added in v0.9.0

type BuilderWalletStats struct {
	FromTs         int64  `json:"fromTs"`
	ToTs           int64  `json:"toTs"`
	NewUsers       int    `json:"newUsers"`
	DepositVolume  string `json:"depositVolume"`
	WithdrawVolume string `json:"withdrawVolume"`
}

BuilderWalletStats is the result of GetBuilderWalletStats — taphub-user-service wallet and registration metrics for the agency. Decimal fields are strings; WithdrawVolume is a positive magnitude. FromTs/ToTs are the resolved window as unix seconds.

type BuilderWalletStatsInput added in v0.9.0

type BuilderWalletStatsInput struct {
	Period RankPeriod
	Range  *TimeRange
}

BuilderWalletStatsInput selects the window for GetBuilderWalletStats. Same selector as BuilderStatsInput — Period required, Range required iff Period == RankPeriodCustom. The SDK converts Range to the user-service's unix-second wire shape internally.

type CallOption

type CallOption func(*callConfig)

CallOption customizes a single SDK call. Currently only retry behavior is configurable; the option pattern leaves room for future fields (e.g., custom headers) without breaking the call signatures.

func WithRetry

func WithRetry(rc RetryConfig) CallOption

WithRetry overrides the client-level retry config for a single call.

func WithRetryDisabled

func WithRetryDisabled() CallOption

WithRetryDisabled forces a single call to fail-fast regardless of the client's retry config.

func WithUserToken added in v0.5.0

func WithUserToken(jwt string) CallOption

WithUserToken forwards a TapHub user JWT on grid-api calls via the x-taphub-authorization header, so the call resolves the user's identity in addition to the agency (S2S). Used by Builder.GetMyRank to resolve the caller's own leaderboard rank (rank_me) — the public GetBuilderLeaderboard board carries no per-user data and does not need it. The token is the AccessToken returned by Auth.LoginS2S / Auth.UserLogin. Ignored by user-service operations.

Added in v0.5.0.

type Client

type Client struct {
	User    *UserModule
	Wallet  *WalletModule
	Auth    *AuthModule
	Builder *BuilderModule
	// contains filtered or unexported fields
}

Client is the SDK entry point. After construction, Client is goroutine-safe and can be shared across the process.

func New

func New(cfg Config) (*Client, error)

New validates the config and returns a ready-to-use Client. The endpoint must use https:// or http://localhost; all five string fields must be non-empty.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context, opts ...CallOption) (json.RawMessage, error)

Ping issues a connectivity + auth check against /api/user/v1/agency/ping.

type Config

type Config struct {
	APIKey       string
	AgencyID     string
	AgencySecret string
	BuilderCode  string
	Endpoint     string
	HTTPClient   *http.Client
	Retry        *RetryConfig
}

Config carries the credentials and transport hooks required to construct a Client. AgencySecret is stored on the returned Client but is never surfaced via Config.String() or MarshalJSON. BuilderCode is an identifier the backend uses to resolve the canonical agency UUID via the `x-builder-code` header; it is NOT a secret and is rendered verbatim.

func (Config) MarshalJSON

func (c Config) MarshalJSON() ([]byte, error)

MarshalJSON emits the JSON representation with the secret redacted.

func (Config) String

func (c Config) String() string

String returns a textual representation with the secret redacted. Used implicitly by fmt verbs.

type Direction

type Direction string

Direction is the deposit/withdraw enum for Wallet.Transfer.

const (
	DirectionDeposit  Direction = "deposit"
	DirectionWithdraw Direction = "withdraw"
)

type Error

type Error struct {
	Kind       ErrorKind
	Code       string
	Status     int
	Message    string
	RetryAfter time.Duration
	Cause      error
	Details    json.RawMessage
	// Meta carries errors[0].extensions.meta from the GraphQL response — the
	// backend's channel for structured, machine-readable error data. Nil when
	// the backend supplies none. Multi-region's Auth_WrongRegion uses it to name
	// the region the caller must retry at; read it via WrongRegionTarget rather
	// than indexing this map by hand.
	Meta map[string]any
}

Error is the single error type returned by every SDK operation. Callers use errors.Is against the sentinel values to branch on Kind, and errors.As to recover the typed *Error for inspecting Code, Status, RetryAfter, etc.

The agency secret and the computed HMAC signature are never written into any field of Error.

func (*Error) Error

func (e *Error) Error() string

Error renders a message that never contains the secret or signature.

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches sentinel values by Kind so callers can write `errors.Is(err, taphubserver.ErrAuth)`.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying cause so errors.As can dig past the SDK wrapper into transport-level errors (net.OpError, context.Canceled, ...).

type ErrorKind

type ErrorKind int

ErrorKind classifies SDK errors. Callers should branch on Kind via errors.Is against the exported sentinel values rather than string-matching on Error().

const (
	KindUnknown ErrorKind = iota
	KindValidation
	KindAuth
	KindNotFound
	KindConflict
	KindRateLimit
	KindInternal
	KindNetwork
)

func (ErrorKind) String

func (k ErrorKind) String() string

String returns a stable identifier for the Kind. Useful for logging.

type LoginS2SInput added in v0.2.0

type LoginS2SInput struct {
	PartnerUserID string
	DisplayName   string

	// ClientMeta is the stringified-JSON blob the builder's own frontend
	// produced (tz, screen, lang, …). The backend stores it under
	// login_histories.meta.client, and for multi-geo it is also the carrier for
	// the FE's nearest-region probe result under the key "preferredRegion",
	// which the backend validates against its deployed regions when pinning a
	// first-join user.
	//
	// Forwarded VERBATIM: the SDK never parses or re-encodes it, because
	// re-marshalling would rewrite an audit record the SDK does not own.
	// Optional — nil omits the field entirely; a non-nil pointer to "" is sent
	// as an empty blob.
	ClientMeta *string

	// Region is the caller's CURRENT region, used only as a first-join pinning
	// candidate; for an already-pinned user the stored home region always wins.
	// Optional — empty omits the field.
	//
	// A builder that geo-routes instead (sending the first login directly to
	// the nearest cluster, which then pins by its own REGION) can leave this
	// empty; that is what the reference builder does.
	Region string
}

LoginS2SInput drives the server-to-server login. PartnerUserID is the builder's own identifier for the end-user (formerly agency_player_id); DisplayName is a human-readable label that the backend stores at first touch.

type MyRank added in v0.6.0

type MyRank struct {
	Rank  int        `json:"rank"`
	Entry *RankEntry `json:"entry,omitempty"`
	Total int        `json:"total"`
}

MyRank is the caller's own row + rank in the full pool (bid-260606-leaderboard-split-my-rank). Entry is nil when the caller has no activity in the window; Rank is then 0. Total is the ranked-pool size, so a client can render "#Rank of Total" without fetching the board.

type MyRankInput added in v0.6.0

type MyRankInput struct {
	Period  RankPeriod
	Range   *TimeRange // required iff Period == RankPeriodCustom
	Sort    RankSort   // "" → server default (pnl)
	SortDir SortDir    // "" → server default (desc)
}

MyRankInput selects the window for GetMyRank — the same selector as the board minus pagination (which is meaningless for a single caller's rank).

type RankEntry added in v0.5.0

type RankEntry struct {
	UserID string `json:"userId"`
	// AgencyUID is the builder's own user id for the user. The server returns it to S2S
	// (authenticated-builder) callers on every leaderboard read — GetBuilderUsersStats,
	// GetBuilderLeaderboard, and GetMyRank — and returns null for anonymous / end-user
	// (non-S2S) callers, since it is the builder's private (often PII) identifier. nil here
	// means a non-S2S caller or an unresolved id. Added in v0.7.0; board + myRank selection
	// added in v0.8.0 (bid-260609).
	AgencyUID *string `json:"agencyUid,omitempty"`
	LoginBy   string  `json:"loginBy"`
	Username  string  `json:"username"`
	Rank      int     `json:"rank"`
	Pnl       string  `json:"pnl"`
	Vol       string  `json:"vol"`
	Payout    string  `json:"payout"`
	Wins      int     `json:"wins"`
	Bids      int     `json:"bids"`
	IsNewUser bool    `json:"isNewUser"`
}

RankEntry is one row of the leaderboard. The Decimal fields (Pnl, Vol, Payout) are serialized as strings to preserve precision.

type RankPeriod added in v0.5.0

type RankPeriod string

RankPeriod selects the leaderboard window. Mirrors grid-api's RankPeriod enum. today includes the open hour; custom requires an explicit hour-aligned Range.

const (
	RankPeriodToday     RankPeriod = "today"
	RankPeriodYesterday RankPeriod = "yesterday"
	RankPeriodLast7d    RankPeriod = "last_7d"
	RankPeriodLast30d   RankPeriod = "last_30d"
	RankPeriodAllTime   RankPeriod = "all_time"
	RankPeriodCustom    RankPeriod = "custom"
)

type RankSort added in v0.5.0

type RankSort string

RankSort selects the ordering metric: pnl = net profit (payout - vol), vol = wagered volume.

const (
	RankSortPnl RankSort = "pnl"
	RankSortVol RankSort = "vol"
)

type RankTimeRange added in v0.5.0

type RankTimeRange struct {
	From time.Time `json:"from"`
	To   time.Time `json:"to"`
}

RankTimeRange is the resolved (hour-aligned) window grid-api actually applied — useful for rendering "for the window X–Y". grid-api returns these as ISO 8601 / RFC3339 strings; encoding/json decodes them into time.Time.

type RetentionCohortRow added in v0.11.0

type RetentionCohortRow struct {
	CohortFrom  time.Time         `json:"cohortFrom"`
	CohortTo    time.Time         `json:"cohortTo"`
	CohortUsers int               `json:"cohortUsers"`
	Periods     []RetentionPeriod `json:"periods"`
}

RetentionCohortRow is one cohort: every user active in [CohortFrom, CohortTo). Cohorts overlap by design (rolling retention — a user active in several periods belongs to several cohorts). CohortUsers is final: a cohort period is always fully in the past.

type RetentionGranularity added in v0.11.0

type RetentionGranularity string

RetentionGranularity is the calendar bucket both retention reads validate against. Mirrors grid-api's RetentionGranularity enum.

const (
	RetentionGranularityDay   RetentionGranularity = "day"
	RetentionGranularityWeek  RetentionGranularity = "week"
	RetentionGranularityMonth RetentionGranularity = "month"
)

type RetentionKpiCard added in v0.11.0

type RetentionKpiCard struct {
	PreviousRangeUsers int `json:"previousRangeUsers"`
	CurrentRangeUsers  int `json:"currentRangeUsers"`
}

RetentionKpiCard is the result of GetRetentionKpiCard. CurrentRangeUsers is the INTERSECTION with the cohort — users counted in PreviousRangeUsers who were also active in CurrentRange — never an independent count of the current window, so it is always <= PreviousRangeUsers. Retention rate = CurrentRangeUsers / PreviousRangeUsers (caller-side; undefined on a 0 cohort).

type RetentionKpiCardInput added in v0.11.0

type RetentionKpiCardInput struct {
	ProductType   RetentionProductType
	Granularity   RetentionGranularity
	PreviousRange TimeRange
	CurrentRange  TimeRange
}

RetentionKpiCardInput drives GetRetentionKpiCard. PreviousRange is the COHORT window, CurrentRange the return window; each must be exactly one calendar bucket of Granularity. The agency is always the configured BuilderCode — there is no agency field on purpose.

type RetentionPeriod added in v0.11.0

type RetentionPeriod struct {
	PeriodNumber  int       `json:"periodNumber"`
	TargetFrom    time.Time `json:"targetFrom"`
	TargetTo      time.Time `json:"targetTo"`
	RetainedUsers *int      `json:"retainedUsers"`
}

RetentionPeriod is one matrix cell: how many of the cohort's users were active again in [TargetFrom, TargetTo). The null rule is load-bearing: RetainedUsers is nil ONLY when the target period has not started; a started-but-in-progress period carries a partial, still-rising number, and 0 means the period was read and nobody returned. Never coalesce nil to 0 — that would draw total churn where there is only future.

type RetentionProductType added in v0.11.0

type RetentionProductType string

RetentionProductType selects the product line a retention read applies to. Mirrors grid-api's ProductType enum. Only prediction_grid is served today; the other values are rejected by the server with Retention_ProductUnavailable rather than answered with a false 0.

const (
	RetentionProductPredictionGrid     RetentionProductType = "prediction_grid"
	RetentionProductPredictionSpot     RetentionProductType = "prediction_spot"
	RetentionProductPredictionLeverage RetentionProductType = "prediction_leverage"
)

type RetentionTrend added in v0.11.0

type RetentionTrend struct {
	ProductType RetentionProductType `json:"productType"`
	Granularity RetentionGranularity `json:"granularity"`
	DataThrough time.Time            `json:"dataThrough"`
	MaxPeriod   int                  `json:"maxPeriod"`
	Rows        []RetentionCohortRow `json:"rows"`
}

RetentionTrend is the cohort-retention matrix, newest cohort first, rows never omitted (an empty cohort is a 0-user row, so trend lines have no gaps). MaxPeriod is 7|8|12 by granularity and every row's Periods has exactly that many entries. DataThrough is the freshness of the served rollup — compare a cell's TargetFrom against it to identify the partial trailing diagonal client-side.

type RetentionTrendInput added in v0.11.0

type RetentionTrendInput struct {
	ProductType RetentionProductType
	Granularity RetentionGranularity
	Anchor      TimeRange
}

RetentionTrendInput drives GetRetentionTrend. Anchor names ONE calendar period of Granularity (normally the current one) — it anchors the grid and does NOT restrict which cohorts return: the server derives the whole matrix backwards from it (newest cohort = anchor − 1 period). A future anchor is rejected; a past anchor reviews a historical trend.

type RetryConfig

type RetryConfig struct {
	Disabled    bool
	MaxAttempts int
	BaseDelay   time.Duration
	MaxDelay    time.Duration
	OnRetry     func(RetryInfo)
}

RetryConfig controls the retry loop. A nil *RetryConfig on Config.Retry applies the defaults; an explicit Disabled=true short-circuits to single-shot.

type RetryInfo

type RetryInfo struct {
	Attempt   int
	Err       *Error
	NextDelay time.Duration
}

RetryInfo is delivered to OnRetry immediately before a retry sleep.

type SortDir added in v0.5.0

type SortDir string

SortDir is the sort direction.

const (
	SortDirAsc  SortDir = "asc"
	SortDirDesc SortDir = "desc"
)

type TimeRange added in v0.5.0

type TimeRange struct {
	From time.Time
	To   time.Time
}

TimeRange is an hour-aligned UTC window. Required when BuilderLeaderboardInput.Period is RankPeriodCustom; ignored otherwise. The bounds are sent to grid-api as ISO 8601 / RFC3339 (yyyy-mm-ddThh:mm:ssZ).

type UserCreateParams

type UserCreateParams struct {
	AgencyUID string
	Username  string
}

UserCreateParams is the input to Create. Field names mirror the TS SDK; they are converted to snake_case on the wire.

type UserFundingSummary added in v0.11.0

type UserFundingSummary struct {
	// echoed from the request, so a zero-valued summary is still self-describing
	AgencyUID string
	Currency  string
	// the window the server actually served; for an all-time call, To is the only record of
	// where the books were closed
	From time.Time
	To   time.Time

	DepositCount   int
	WithdrawCount  int
	DepositAmount  string // decimal string, positive
	WithdrawAmount string // decimal string, positive magnitude

	PendingDepositCount   int
	PendingWithdrawCount  int
	PendingDepositAmount  string
	PendingWithdrawAmount string
}

UserFundingSummary is the typed result of GetUserFundingSummary.

Amounts are decimal strings, never float64 — reconciliation exists to detect discrepancies too small to survive a floating-point round trip. Feed them to shopspring/decimal if you need arithmetic.

Both withdraw amounts are POSITIVE magnitudes even though withdrawals are stored negative backend-side, matching the sign convention of the builder wallet stats surface. So DepositAmount - WithdrawAmount is the net funding flow, and callers never need to know the storage convention.

Pending* carries the same four numbers for transactions still in flight. They are reported beside the finished ones rather than folded in, because that is where reconciliation mismatches live: a total short by 50 is a different problem depending on whether the 50 is pending or lost.

type UserFundingSummaryParams added in v0.11.0

type UserFundingSummaryParams struct {
	AgencyUID string
	Currency  string
	From      *time.Time // nil = all-time
	To        *time.Time // nil = now (server-resolved)
}

UserFundingSummaryParams is the input to GetUserFundingSummary.

AgencyUID and Currency are both required. Currency is not optional by oversight: a user may hold several wallets, and summing across currencies yields a number that is arithmetically valid and semantically meaningless — the caller must state which book they are reconciling.

From and To are optional and half-open: From inclusive, To exclusive, so consecutive windows tile without double-counting a boundary transaction. Nil From means all-time; nil To means the instant the server serves the query. Pointers rather than zero-value sentinels because time.Time{} is a real instant (year 1), and treating it as "unset" would silently turn an uninitialized struct field into an all-time query.

type UserModule

type UserModule struct {
	// contains filtered or unexported fields
}

UserModule groups user-domain operations under Client.User.

func (*UserModule) Create

func (m *UserModule) Create(
	ctx context.Context,
	p UserCreateParams,
	opts ...CallOption,
) (json.RawMessage, error)

Create provisions a user under the calling agency.

type Wallet

type Wallet struct {
	ID        string
	Amount    string
	Currency  string
	IsEnabled bool
}

Wallet is the domain type returned by GetUserWallet and ListUserWallets. Amount is a decimal string mirroring the backend decimal.Decimal.

type WalletModule

type WalletModule struct {
	// contains filtered or unexported fields
}

WalletModule groups wallet-domain operations under Client.Wallet.

func (*WalletModule) Cancel added in v0.3.0

func (m *WalletModule) Cancel(
	ctx context.Context,
	ref string,
	opts ...CallOption,
) (*WalletReservation, error)

Cancel releases a PENDING reservation: PENDING → FAILED. The wallet's Amount is restored (reservation funds are returned to the spendable balance) and ReservedAmount is decremented.

Idempotent on already-FAILED rows. Returns *Error with code ErrReservationAlreadyCommitted if the row is FINISHED, or code ErrReservationNotFound if the ref is unknown (or belongs to a different agency).

Backend endpoint: GraphQL mutation `agencyCancel` on /th-user-gql. The legacy REST endpoint POST /api/user/v1/agency/cancel is deprecated.

func (*WalletModule) Commit added in v0.3.0

func (m *WalletModule) Commit(
	ctx context.Context,
	ref string,
	opts ...CallOption,
) (*WalletReservation, error)

Commit finalizes a PENDING reservation referenced by ref: PENDING → FINISHED. The wallet's Amount is unchanged (it was already adjusted at reserve-time); only ReservedAmount is decremented.

Idempotent on already-FINISHED rows. Returns *Error with code ErrReservationAlreadyCancelled if the row is FAILED, or code ErrReservationNotFound if the ref is unknown (or belongs to a different agency).

Backend endpoint: GraphQL mutation `agencyCommit` on /th-user-gql. The legacy REST endpoint POST /api/user/v1/agency/commit is deprecated.

func (*WalletModule) GetTxByRef

func (m *WalletModule) GetTxByRef(
	ctx context.Context,
	ref string,
	opts ...CallOption,
) (*WalletTransaction, error)

GetTxByRef looks up a wallet transaction by the idempotency ref passed to Transfer. Returns *Error with Kind=KindNotFound when the ref is unknown.

func (*WalletModule) GetUserFundingSummary added in v0.11.0

func (m *WalletModule) GetUserFundingSummary(
	ctx context.Context,
	p UserFundingSummaryParams,
	opts ...CallOption,
) (*UserFundingSummary, error)

GetUserFundingSummary returns the aggregated deposit/withdraw activity of one of the builder's own users (by agencyUid), in one currency, over an optional [From, To) window — the per-user counterpart of the agency-wide wallet stats.

It is an HMAC-signed S2S call to taphub-user-service. The agency comes from the client's configured builder credentials, never from an argument, so a builder can only read its own users. An unknown agencyUid returns an all-zero summary rather than an error; so does a uid belonging to another agency, deliberately — the two are indistinguishable, so this call cannot be used to probe whether a uid exists under some other builder.

Requires a backend carrying the builderUserFundingSummary query (OpenSpec change builder-260820-funding-reconciliation); older deployments reject it as an unknown field. Transfer-mode agencies only — seamless agencies keep transactions in a different table and are rejected with TransferModeNotSupported rather than answered with zeros.

LIMIT OF THIS DATA — read before drawing conclusions. These numbers cover only what TapHub recorded. A request that never reached TapHub (a network failure before the write) appears in none of them. Comparing balances alone therefore cannot distinguish "a request was lost" from "the ledger is correct": both look identical. Reconcile the COUNT of requests you issued against DepositCount / WithdrawCount, not merely the amounts.

func (*WalletModule) GetUserWallet

func (m *WalletModule) GetUserWallet(
	ctx context.Context,
	agencyUID string,
	opts ...CallOption,
) (*Wallet, error)

GetUserWallet returns the agency's configured-currency wallet for the user.

func (*WalletModule) ListUserWallets

func (m *WalletModule) ListUserWallets(
	ctx context.Context,
	agencyUID string,
	opts ...CallOption,
) ([]Wallet, error)

ListUserWallets returns every wallet the user owns under the agency.

As of builder-260627 this calls the GraphQL `builderUserWallets` query on the user-service (HMAC S2S) instead of the deprecated REST list-user-wallet endpoint. Same agency-scoped semantics — the agency is resolved from the configured BuilderCode, never passed as an argument — and the same Wallet payload (incl. IsEnabled). An unknown agencyUID returns an empty slice.

func (*WalletModule) Reserve added in v0.3.0

Reserve creates a PENDING wallet reservation against the agency wallet.

Funds are moved out of Wallet.Amount and into Wallet.ReservedAmount atomically. The reservation must be finalized via Commit or released via Cancel before TTLSeconds elapses (default 1800s, clamped by the backend to [60, 86400]); otherwise the backend sweeper auto-cancels.

Ref is the idempotency key. Replay with the same Ref + matching body returns the same row; replay with a different amount or currency returns a *Error with Kind=KindConflict and code ErrTxRefConflict.

Backend endpoint: GraphQL mutation `agencyReserve` on /th-user-gql. The legacy REST endpoint POST /api/user/v1/agency/reserve is deprecated since v0.4.0 — the backend logs a `[deprecated]` WARN per call.

func (*WalletModule) Transfer

Transfer moves funds to or from a user's wallet. Ref is the idempotency key. The SDK always sends a positive Amount; for withdrawals it prepends `-` to the wire value, matching the backend convention where the sign of `amount` IS the direction (no separate `direction` field).

type WalletReservation added in v0.3.0

type WalletReservation struct {
	ID           string
	WalletID     string
	UserID       string
	Amount       string
	BeforeAmount string
	AfterAmount  string
	Ref          string // from wire field tx_ref
	Status       string // PENDING | FINISHED | FAILED
	TTLSeconds   *int
	ExpiresAt    string // RFC3339; empty when backend omits
	Timestamp    string
}

WalletReservation is the typed return of Reserve, Commit, and Cancel. Mirrors the backend WalletReservationResVO and the documented AgencyWalletReservation GraphQL type.

type WalletReserveParams added in v0.3.0

type WalletReserveParams struct {
	Ref        string
	AgencyUID  string
	Amount     string // positive decimal string; SDK negates on the wire
	Direction  Direction
	Currency   string
	TTLSeconds *int // optional. Backend clamps to [60, 86400], default 1800
}

WalletReserveParams is the input to Reserve. Mirrors WalletTransferParams plus an optional TTLSeconds override.

As of backend OpenSpec change user-260527-agency-wallet-reserve VULN-002, the backend rejects positive amounts (Direction=DirectionDeposit). The SDK fails fast at the validation layer to avoid an HTTP round-trip — only Direction=DirectionWithdraw is accepted. This restriction will be lifted when commit-of-deposit semantics are designed.

type WalletTransaction

type WalletTransaction struct {
	ID           string
	WalletID     string
	UserID       string
	Amount       string
	BeforeAmount string
	AfterAmount  string
	Currency     string
	Ref          string // from wire field tx_ref
	Source       string
	Type         string
	// Timestamp is the RFC3339 placement time. Populated by GetBuilderUserTransactions
	// (builder-260629); GetTxByRef leaves it empty (its wire shape omits it).
	Timestamp string
}

WalletTransaction is the domain type returned by GetTxByRef and GetBuilderUserTransactions. Field names are Go-camelCase; the wire-side representation uses snake_case.

type WalletTransferParams

type WalletTransferParams struct {
	Ref       string
	AgencyUID string
	Amount    string
	Direction Direction
	Currency  string
}

WalletTransferParams is the input to Transfer. Ref is mandatory for idempotency; the backend dedupes duplicate refs with 409 Conflict. Currency must match the agency configuration; the backend rejects mismatches unless skip_currency_validation is set (which the SDK never sends). Amount must always be a positive decimal string — Direction is the single source of truth for sign, and the SDK negates the wire value for withdrawals.

type WalletTransferResult added in v0.2.0

type WalletTransferResult struct {
	ID           string
	WalletID     string
	UserID       string
	Amount       string
	BeforeAmount string
	AfterAmount  string
	Ref          string // from wire field tx_ref
	Timestamp    string
}

WalletTransferResult is the typed return of Transfer in v0.2.0+. Mirrors the TS SDK's WalletTransferResult and the backend WalletTransactionReqVO (which intentionally omits currency, source, type — those are implied by the request input or by the sign of amount).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL