tokentip

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 15 Imported by: 0

README

tokentip — Go SDK

Tip my tokens

Official Go client for the TokenTip API. Tips that buy AI inference credit for open-source maintainers. Zero dependencies outside the standard library — typed models, RFC 7807 errors, webhook verification, and a live event stream.

go get github.com/copyleftdev/tokentip-go

Requires Go 1.23+.

Quickstart

Mint an API token in your dashboard (tt_…), then:

package main

import (
	"context"
	"fmt"

	"github.com/copyleftdev/tokentip-go"
)

func main() {
	tt := tokentip.New("tt_your_token")
	ctx := context.Background()

	me, err := tt.Me(ctx)
	if err != nil {
		panic(err)
	}
	fmt.Println(me.Handle, me.Credit) // Money is an exact decimal string

	tips, _ := tt.ListTips(ctx, &tokentip.ListOptions{Status: "settled", Limit: 20})
	for _, tip := range tips.Data {
		fmt.Println(tip.ID, tip.NetToTokens)
	}
}

Money fields are the tokentip.Money type (a decimal string) — exact, never a float.

Configuration

tt := tokentip.New("tt_your_token",
	tokentip.WithBaseURL("https://tokentip.to"),
	tokentip.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)

The live event stream (SSE)

stream, err := tt.StreamEvents(ctx, false) // true = operator, system-wide
if err != nil {
	panic(err)
}
defer stream.Close()

for {
	ev, err := stream.Next()
	if err == io.EOF {
		break
	}
	if err != nil {
		panic(err)
	}
	switch ev.Type {
	case "tip.settled":
		fmt.Println("credit granted:", ev.Tip().NetToTokens, "→ balance", *ev.Balance())
	case "key.rotated":
		fmt.Println("key rotated")
	}
}

Events carry an idempotency ID — dedupe on it, TokenTip may redeliver.

Webhooks

Register an endpoint and receive the same events over signed POSTs:

created, _ := tt.CreateWebhook(ctx, tokentip.NewWebhook{
	URL:    "https://you.example/hook",
	Events: []string{"tip.settled"},
})
fmt.Println(created.Secret) // whsec_… — shown once, store it now

Verify each delivery in your HTTP handler (constant-time, timestamp-bounded):

func handle(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	ev, err := tokentip.VerifyWebhook(
		body,
		r.Header.Get("X-TokenTip-Signature"),
		signingSecret,             // the whsec_… from CreateWebhook
		tokentip.DefaultTolerance, // or 0 for the default
	)
	if err != nil {
		http.Error(w, "bad signature", http.StatusBadRequest)
		return
	}
	log.Println("verified", ev.Type)
}

Operator

With an operator-scoped token:

overview, _ := tt.OperatorOverview(ctx)
fmt.Println(overview.Pool.State, overview.Pool.Committed)

tt.OperatorSettle(ctx, tipID, tipID)       // force-settle; idempotency key = tipID
tt.OperatorDisableKey(ctx, "some-handle")  // fraud kill-switch

Errors

Non-2xx responses return a *tokentip.Error carrying the problem detail. Test the status with the helpers or errors.As:

_, err := tt.GetTip(ctx, "does-not-exist")
if tokentip.IsNotFound(err) {
	// 404
}

var apiErr *tokentip.Error
if errors.As(err, &apiErr) {
	fmt.Println(apiErr.Status, apiErr.Title, apiErr.Detail)
}

Helpers: IsUnauthorized, IsForbidden, IsNotFound, IsConflict, IsUpstream.

Documentation

Overview

Package tokentip is the official Go client for the TokenTip API — tips that buy AI inference credit for open-source maintainers. It has no dependencies outside the standard library.

Index

Constants

View Source
const DefaultTolerance = 5 * time.Minute

DefaultTolerance is the accepted clock skew between the signature timestamp and now when verifying a webhook.

Variables

View Source
var ErrInvalidSignature = errors.New("tokentip: invalid webhook signature")

ErrInvalidSignature is returned by VerifyWebhook when a delivery cannot be trusted — malformed header, timestamp outside tolerance, or a signature mismatch.

Functions

func IsConflict

func IsConflict(err error) bool

func IsForbidden

func IsForbidden(err error) bool

func IsNotFound

func IsNotFound(err error) bool

func IsUnauthorized

func IsUnauthorized(err error) bool

func IsUpstream

func IsUpstream(err error) bool

Types

type Ack

type Ack struct {
	OK      bool   `json:"ok"`
	Message string `json:"message"`
}

type ApiToken added in v0.2.0

type ApiToken struct {
	ID         string     `json:"id"`
	Prefix     string     `json:"prefix"`
	Name       *string    `json:"name"`
	Scope      string     `json:"scope"`
	CreatedAt  time.Time  `json:"created_at"`
	LastUsedAt *time.Time `json:"last_used_at"`
}

type Balance

type Balance struct {
	Credit     Money  `json:"credit"`
	KeyClaimed bool   `json:"key_claimed"`
	LiveUsage  *Money `json:"live_usage"`
	LiveLimit  *Money `json:"live_limit"`
	Disabled   *bool  `json:"disabled"`
}

type Client

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

func New

func New(token string, opts ...Option) *Client

New returns a client authenticated with a tt_ API token.

func (*Client) Balance

func (c *Client) Balance(ctx context.Context) (*Balance, error)

func (*Client) CreateWebhook

func (c *Client) CreateWebhook(ctx context.Context, in NewWebhook) (*WebhookEndpointCreated, error)

func (*Client) DeleteWebhook

func (c *Client) DeleteWebhook(ctx context.Context, id string) error

func (*Client) GetKey

func (c *Client) GetKey(ctx context.Context) (*Key, error)

func (*Client) GetTip

func (c *Client) GetTip(ctx context.Context, id string) (*Tip, error)

func (*Client) ListTips

func (c *Client) ListTips(ctx context.Context, opts *ListOptions) (*TipList, error)

func (*Client) ListTokens added in v0.2.0

func (c *Client) ListTokens(ctx context.Context) ([]ApiToken, error)

func (*Client) ListWebhooks

func (c *Client) ListWebhooks(ctx context.Context) ([]WebhookEndpoint, error)

func (*Client) Me

func (c *Client) Me(ctx context.Context) (*Me, error)

func (*Client) OperatorDisableKey

func (c *Client) OperatorDisableKey(ctx context.Context, handle string) (*Ack, error)

func (*Client) OperatorListCreators

func (c *Client) OperatorListCreators(ctx context.Context, opts *ListOptions) (*CreatorList, error)

func (*Client) OperatorListTips

func (c *Client) OperatorListTips(ctx context.Context, opts *ListOptions) (*TipList, error)

func (*Client) OperatorOverview

func (c *Client) OperatorOverview(ctx context.Context) (*Overview, error)

func (*Client) OperatorSettle

func (c *Client) OperatorSettle(ctx context.Context, tipID, idempotencyKey string) (*ForceSettleResult, error)

func (*Client) RevokeToken added in v0.2.0

func (c *Client) RevokeToken(ctx context.Context, id string) error

func (*Client) RotateKey

func (c *Client) RotateKey(ctx context.Context, idempotencyKey string) (*MintedKey, error)

func (*Client) Stats

func (c *Client) Stats(ctx context.Context) (*Stats, error)

func (*Client) StreamEvents

func (c *Client) StreamEvents(ctx context.Context, operator bool) (*EventStream, error)

StreamEvents opens the creator stream (GET /v1/events), or the system-wide operator stream when operator is true. The stream stays open until the context is cancelled or Close is called.

type CreatorList

type CreatorList struct {
	Data       []CreatorSummary `json:"data"`
	NextCursor *string          `json:"next_cursor"`
}

type CreatorSummary

type CreatorSummary struct {
	Handle       string     `json:"handle"`
	Balance      Money      `json:"balance"`
	KeyClaimed   bool       `json:"key_claimed"`
	DisplayName  *string    `json:"display_name"`
	Fingerprint  *string    `json:"fingerprint"`
	LiveUsage    *Money     `json:"live_usage"`
	LiveLimit    *Money     `json:"live_limit"`
	Disabled     *bool      `json:"disabled"`
	SettledTips  *int       `json:"settled_tips"`
	SettledGross *Money     `json:"settled_gross"`
	Joined       *time.Time `json:"joined"`
}

type Error

type Error struct {
	Status   int    `json:"status"`
	Title    string `json:"title"`
	Detail   string `json:"detail"`
	Type     string `json:"type"`
	Instance string `json:"instance"`
}

Error is the typed form of an RFC 7807 problem+json response.

func (*Error) Error

func (e *Error) Error() string

type Event

type Event struct {
	ID         string          `json:"id"`
	Type       string          `json:"type"`
	Created    time.Time       `json:"created"`
	Scope      string          `json:"scope"`
	APIVersion string          `json:"api_version"`
	Data       json.RawMessage `json:"data"`
}

Event is one item on the live stream or a webhook delivery. Data holds the event-specific payload; use Tip / Balance or Decode to read it.

func VerifyWebhook

func VerifyWebhook(payload []byte, signatureHeader, secret string, tolerance time.Duration) (*Event, error)

VerifyWebhook checks the X-TokenTip-Signature header against the raw request body and the endpoint's signing secret, then returns the decoded event. Pass tolerance == 0 to use DefaultTolerance. It returns ErrInvalidSignature on a malformed header, a stale timestamp, or a signature mismatch.

func (*Event) Balance

func (e *Event) Balance() *Money

Balance returns the post-transition credit balance on settled/disputed events.

func (*Event) Decode

func (e *Event) Decode(v any) error

Decode unmarshals the event's data payload into v.

func (*Event) Tip

func (e *Event) Tip() *Tip

Tip returns the tip carried by a tip.* event, or nil if the payload has none.

type EventStream

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

EventStream is a live server-sent-events subscription to the tip lifecycle. Call Next until it returns io.EOF, then Close.

func (*EventStream) Close

func (s *EventStream) Close() error

func (*EventStream) Next

func (s *EventStream) Next() (*Event, error)

Next blocks until the next event arrives, returning io.EOF when the stream ends. Events carry an idempotency ID; dedupe on it, as TokenTip may redeliver.

type ForceSettleResult

type ForceSettleResult struct {
	Status  string `json:"status"`
	Message string `json:"message"`
}

type Key

type Key struct {
	Fingerprint string     `json:"fingerprint"`
	Disabled    bool       `json:"disabled"`
	MintedAt    *time.Time `json:"minted_at"`
	Usage       *Money     `json:"usage"`
	Limit       *Money     `json:"limit"`
}

type ListOptions

type ListOptions struct {
	Limit  int
	Cursor string
	Status string
}

ListOptions are the shared cursor-pagination + filter parameters.

type Me

type Me struct {
	Handle         string     `json:"handle"`
	DisplayName    string     `json:"display_name"`
	Credit         Money      `json:"credit"`
	KeyClaimed     bool       `json:"key_claimed"`
	KeyFingerprint *string    `json:"key_fingerprint"`
	CreatedAt      *time.Time `json:"created_at"`
}

type MintedKey

type MintedKey struct {
	Secret      string     `json:"secret"`
	Fingerprint string     `json:"fingerprint"`
	Limit       Money      `json:"limit"`
	MintedAt    *time.Time `json:"minted_at"`
}

type Money

type Money string

Money is an exact USD amount as a decimal string (six-decimal precision). It is never a float, to avoid binary rounding on money.

type NewWebhook

type NewWebhook struct {
	URL         string   `json:"url"`
	Events      []string `json:"events,omitempty"`
	Description string   `json:"description,omitempty"`
}

type Option

type Option func(*Client)

func WithBaseURL

func WithBaseURL(u string) Option

WithBaseURL overrides the API host (for testing or staging).

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies a custom *http.Client (timeouts, proxies, transport).

type Overview

type Overview struct {
	Pool         Pool             `json:"pool"`
	Counts       map[string]int   `json:"counts"`
	Money        map[string]Money `json:"money"`
	TipsByStatus map[string]int   `json:"tips_by_status"`
}

type Pool

type Pool struct {
	Committed Money  `json:"committed"`
	State     string `json:"state"`
	Available *Money `json:"available"`
	Headroom  *Money `json:"headroom"`
}

type Stats

type Stats struct {
	TipsTotal     int   `json:"tips_total"`
	TipsSettled   int   `json:"tips_settled"`
	TipsHeld      int   `json:"tips_held"`
	SettledGross  Money `json:"settled_gross"`
	SettledNet    Money `json:"settled_net"`
	LifetimeGross Money `json:"lifetime_gross"`
}

type Tip

type Tip struct {
	ID            string     `json:"id"`
	CreatorHandle string     `json:"creator_handle"`
	Status        string     `json:"status"`
	Gross         Money      `json:"gross"`
	NetToTokens   Money      `json:"net_to_tokens"`
	TipperName    *string    `json:"tipper_name"`
	TipperNote    *string    `json:"tipper_note"`
	CreatedAt     time.Time  `json:"created_at"`
	HeldAt        *time.Time `json:"held_at"`
	SettledAt     *time.Time `json:"settled_at"`
}

type TipList

type TipList struct {
	Data       []Tip   `json:"data"`
	NextCursor *string `json:"next_cursor"`
}

type WebhookEndpoint

type WebhookEndpoint struct {
	ID        string     `json:"id"`
	URL       string     `json:"url"`
	Events    []string   `json:"events"`
	Disabled  bool       `json:"disabled"`
	CreatedAt *time.Time `json:"created_at"`
}

type WebhookEndpointCreated

type WebhookEndpointCreated struct {
	WebhookEndpoint
	Secret string `json:"secret"`
}

Jump to

Keyboard shortcuts

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