Documentation
¶
Overview ¶
Package goei pushes locally-computed Claude Code spend aggregates to a Goei dashboard's device-token ingest endpoint.
This is the "zero-key" half of Goei's two-track trust model: instead of handing Goei an Anthropic admin API key, the user runs budgetclaw locally (which only ever reads ~/.claude/projects/*.jsonl) and ships aggregated dollar-and-token rollups to their own dashboard. No key leaves the machine; the only thing transmitted is the cost summary.
The wire contract mirrors Goei's POST /api/ingest handler:
- Authorization: Bearer goei_dt_<32-hex> (exactly 40 chars)
- body: {provider, spend[], usage?} provider must be "anthropic"
- spend dedup key: (period_start, model, project, branch)
- usage dedup key: (period_start, metric_type, model, breakdown_key, breakdown_value)
Each spend record also carries an optional inline "tokens" object (input, output, cache_read, cache_write_5m, cache_write_1h) at the same per-(day, project, branch, model) grain as its dollar amount. The current server ignores it and keys off amountCents; a future server prefers tokens so it can re-price at its own point-in-time rate. Both are always sent, so the change is backward compatible.
Per-branch attribution (a budgetclaw differentiator) rides on the spend record's own optional branch field, so the project field always carries the bare project name. With --no-branch the branch is omitted and all branches of a project collapse server-side. Usage records break down by bare project name regardless.
Both arrays dedupe server-side via upsert, so re-running sync is idempotent: the same day re-sent overwrites, it does not double-count.
Index ¶
- Constants
- func HostOf(endpoint string) string
- func IngestEndpointFor(endpoint string) string
- func ValidToken(t string) bool
- type Aggregate
- type Client
- type DeviceAuthPoll
- type DeviceAuthStart
- type GuardEvent
- type Payload
- type PolicyResponse
- type RepoPointer
- type SpendRecord
- type TokenCounts
- type UsageRecord
- type WirePolicy
Constants ¶
const DefaultBaseURL = "https://goei.roninforge.org"
DefaultBaseURL is the production Goei origin. A repo pointer's endpoint normalizes to this when unset.
const DefaultEndpoint = "https://goei.roninforge.org/api/ingest"
DefaultEndpoint is the production Goei ingest URL.
const DefaultHost = "goei.roninforge.org"
DefaultHost is the host of DefaultBaseURL, used to decide whether a repo pointer aims at the default hosted Goei or a self-hosted instance the user should be warned about.
const PointerFileName = ".budgetclaw.toml"
PointerFileName is the committed, non-secret team pointer a Goei team lead drops in a repo root. It carries only routing info plus a revocable join code, never a device token. Its presence is what lets `budgetclaw team join` (and the discovery line) know a team dashboard exists for this repo.
const Provider = "anthropic"
Provider is the only provider value Goei accepts for Claude Code data. Claude Code spend is Anthropic API spend.
Variables ¶
This section is empty.
Functions ¶
func HostOf ¶ added in v1.4.0
HostOf returns the host a pointer endpoint resolves to (empty -> DefaultHost), for display and trust decisions before any request is made.
func IngestEndpointFor ¶ added in v1.4.0
IngestEndpointFor derives the ingest URL from a pointer endpoint so a token saved by `team join` points at the same server the join used.
func ValidToken ¶
ValidToken reports whether a string is shaped like a Goei device token: the "goei_dt_" prefix plus a 32-char body, 40 chars total. This matches the format check Goei's ingest handler enforces.
Types ¶
type Aggregate ¶
type Aggregate struct {
Project string
GitBranch string
Model string
Day string // YYYY-MM-DD (UTC)
CostUSD float64
InputTokens int
OutputTokens int
CacheReadTokens int
CacheWrite5mTokens int
CacheWrite1hTokens int
}
Aggregate is the neutral input to BuildPayloads, decoupled from the db package so this package has no storage dependency. It mirrors db.SyncAggregate.
type Client ¶
Client posts payloads to a Goei ingest endpoint.
func New ¶
New returns a Client with sane defaults. An empty endpoint falls back to DefaultEndpoint.
func (*Client) PullPolicies ¶ added in v1.3.0
func (c *Client) PullPolicies(ctx context.Context, etag string) (resp *PolicyResponse, newETag string, notModified bool, err error)
PullPolicies fetches this device's Guard Mode policy set from GET /api/policy. A conditional request with the last ETag returns notModified=true and no body when nothing changed, so `budgetclaw watch` can poll cheaply. The returned etag should be passed to the next call.
type DeviceAuthPoll ¶ added in v1.4.0
type DeviceAuthPoll struct {
Status string `json:"status"`
Token string `json:"token,omitempty"`
Team string `json:"team,omitempty"`
}
DeviceAuthPoll is the response to POST /api/team/device/poll. Status is one of pending, requested, approved, completed, denied, expired. Token is present exactly once, on the first poll after the lead approves.
func PollDeviceAuth ¶ added in v1.4.0
func PollDeviceAuth(ctx context.Context, endpoint, deviceCode string) (*DeviceAuthPoll, error)
PollDeviceAuth checks the state of a device authorization. A 429 is treated as a transient "keep waiting" so the caller's poll loop backs off rather than failing.
type DeviceAuthStart ¶ added in v1.4.0
type DeviceAuthStart struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
Interval int `json:"interval"`
ExpiresIn int `json:"expires_in"`
Team string `json:"team"`
}
DeviceAuthStart is the response to POST /api/team/device/start: the device_code the CLI polls with, the short user_code the teammate confirms in a browser, and the URLs and timing to drive the flow.
func StartDeviceAuth ¶ added in v1.4.0
func StartDeviceAuth(ctx context.Context, endpoint, joinCode, machine, deviceName string) (*DeviceAuthStart, error)
StartDeviceAuth exchanges a join code for a device authorization. It mints no credential: the returned codes drive a browser confirmation and a lead approval before any token exists.
type GuardEvent ¶ added in v1.3.0
type GuardEvent struct {
PolicyID string `json:"policyId"`
Action string `json:"action"` // notify | warn | kill | override
ScopeType string `json:"scopeType,omitempty"`
ScopeValue string `json:"scopeValue,omitempty"`
Machine string `json:"machine,omitempty"`
AmountCents int `json:"amountCents"`
CapCents int `json:"capCents"`
At string `json:"at,omitempty"`
// DedupKey makes reporting idempotent: the server INSERT OR IGNOREs on
// it, so a re-sent event (same period, same action) is recorded once.
DedupKey string `json:"dedupKey,omitempty"`
}
GuardEvent is one enforcement audit record: a remote policy warned or killed a runaway on this machine. Content-free by construction (ids, amounts, scope labels, machine, timestamps only), so it upholds the zero-prompt pledge exactly like the spend records do.
type Payload ¶
type Payload struct {
Provider string `json:"provider"`
Spend []SpendRecord `json:"spend"`
Usage []UsageRecord `json:"usage,omitempty"`
// GuardEvents carries content-free enforcement audit records back up
// with the sync (Guard Mode's audit channel). Omitted when there are
// none, so a plain sync is byte-for-byte unchanged.
GuardEvents []GuardEvent `json:"guardEvents,omitempty"`
}
Payload is one POST body to /api/ingest.
func BuildPayloads ¶
BuildPayloads converts aggregates into one or more ingest payloads, chunked to stay under the server's per-request caps. Aggregates are grouped by day and whole days are packed into requests, so every request carries each day's spend and usage together and no request is ever spend-empty (which the endpoint rejects).
A spend record is emitted for every aggregate (including ones that round to zero cents) so that a day with only sub-cent usage still has the spend row its usage rows ride along with. Usage records are emitted only for non-zero token metrics.
machine is stamped on every spend record so the server can attribute rollups to the machine they came from. An empty machine is fine: the omitempty field is dropped and the server treats it as legacy.
type PolicyResponse ¶ added in v1.3.0
type PolicyResponse struct {
PolicyVersion int `json:"policyVersion"`
Policies []WirePolicy `json:"policies"`
}
PolicyResponse is the Guard Mode policy bundle, returned both by GET /api/policy and piggybacked on the POST /api/ingest response.
type RepoPointer ¶ added in v1.4.0
type RepoPointer struct {
Team string
Endpoint string
JoinCode string
Mode string
Path string // absolute path of the file it was read from
// contains filtered or unexported fields
}
RepoPointer is a parsed goei pointer section from a committed .budgetclaw.toml.
func FindRepoPointer ¶ added in v1.4.0
func FindRepoPointer(startDir string) (*RepoPointer, error)
FindRepoPointer walks up from startDir looking for a .budgetclaw.toml that carries a goei join_code, so a teammate can run `budgetclaw team join` from anywhere inside the repo, not only its root. The walk stops at the repo root (the first directory containing a .git entry): a pointer belongs to a specific repository, so a stray .budgetclaw.toml sitting in a shared parent (a home directory, a CI runner) must not be picked up by unrelated repos nested beneath it. Returns (nil, nil) when no pointer is found within the repo (or before the filesystem root when startDir is not in a repo). A malformed pointer file surfaces its parse error.
func ParsePointer ¶ added in v1.4.0
func ParsePointer(data []byte, path string) (*RepoPointer, error)
ParsePointer decodes a pointer file's bytes. It returns (nil, nil) when the file carries no goei join_code, so an unrelated .budgetclaw.toml (for instance a hand-written local config that happens to sit in a repo) is not mistaken for a team pointer.
func (*RepoPointer) PromptsEnabled ¶ added in v1.4.0
func (p *RepoPointer) PromptsEnabled() bool
PromptsEnabled reports whether the discovery line may render for this repo. An absent key means yes; `[goei] prompts = false` in the pointer silences it forever.
type SpendRecord ¶
type SpendRecord struct {
PeriodStart string `json:"periodStart"`
PeriodEnd string `json:"periodEnd"`
AmountCents int `json:"amountCents"`
Currency string `json:"currency"`
Model string `json:"model,omitempty"`
Project string `json:"project,omitempty"`
Branch string `json:"branch,omitempty"`
Machine string `json:"machine,omitempty"`
Tokens *TokenCounts `json:"tokens,omitempty"`
}
SpendRecord is one daily per-(model, project, branch) dollar amount. Branch is optional: when empty (the --no-branch case) the server collapses every branch of a project into a single project-level row.
Machine is an optional per-machine identity (typically the OS hostname). The Goei server uses it to keep two machines' rollups from colliding: the same (day, project, branch, model) synced from a laptop and a desktop stay separate instead of overwriting each other. When empty the server treats it as legacy/unknown, so the field is additive and backward compatible.
Tokens is an optional per-(day, project, branch, model) token rollup at the same grain as AmountCents. The current Goei server ignores it and keys off AmountCents; a future server prefers Tokens so it can re-price at its own point-in-time rate. Both are always sent.
type TokenCounts ¶ added in v1.0.0
type TokenCounts struct {
Input int `json:"input"`
Output int `json:"output"`
CacheRead int `json:"cache_read"`
CacheWrite5m int `json:"cache_write_5m"`
CacheWrite1h int `json:"cache_write_1h"`
}
TokenCounts is the per-(day, project, branch, model) token rollup carried inline on a spend record. It is the same grain as the spend dollar amount, so a future Goei server can re-price the tokens at its own point-in-time rate instead of trusting amountCents. The current server ignores this field; sending it is forward-compatible.
type UsageRecord ¶
type UsageRecord struct {
PeriodStart string `json:"periodStart"`
PeriodEnd string `json:"periodEnd"`
MetricType string `json:"metricType"`
MetricValue int `json:"metricValue"`
Model string `json:"model,omitempty"`
BreakdownKey string `json:"breakdownKey,omitempty"`
BreakdownValue string `json:"breakdownValue,omitempty"`
}
UsageRecord is one daily per-(model, project) token count for a single metric type.
type WirePolicy ¶ added in v1.3.0
type WirePolicy struct {
ID string `json:"id"`
Scope struct {
Type string `json:"type"`
Value string `json:"value"`
} `json:"scope"`
Period string `json:"period"`
CapCents int `json:"capCents"`
Enforcement string `json:"enforcement"`
Action string `json:"action"`
ServerSpentCents int `json:"serverSpentCents"`
AsOf string `json:"asOf"`
SetBy string `json:"setBy"`
Source string `json:"source"`
}
WirePolicy is one budget policy as Goei serializes it for this device on the Guard Mode down-channel. enforcement is "local_exact" (enforce against this machine's own rollup, kill-eligible) or "server_aggregate" (a team-wide figure this device can only warn against, with staleness).