Documentation · API · SDKs · MCP

Feature flags, evaluated inside your process.

wagle is a feature-flag and typed remote-config service whose SDKs evaluate locally: zero network hops, no database on the hot path. These docs take you from nothing to a flag served in your Go or TypeScript app, over a plain HTTP API you can curl, with an MCP endpoint for AI agents. How it works in depth lives on the why & how page; coming from Flagsmith, there is a dedicated migration runbook.

01 · The model

How wagle works, in 30 seconds

Everything below assumes this loop. Your app holds a whole, versioned config snapshot and evaluates against it in-process; wagle's job is to get the next version to you cheaply.

01

Mint a token

In the console, mint an SDK token: a durable, revocable credential, read-only by design. Store it in WAGLE_API_TOKEN.

02

Seed once

The SDK pulls one whole (project, environment) snapshot with its version - flags, segments, rules, all of it.

03

Evaluate locally

Every evaluation is an in-process read of that snapshot: hash the entity into a bucket, walk the rules, serve a variation. No network call, ever.

04

Poll for changes

The SDK re-checks on your interval (default 30s). An unchanged poll gets a tiny “nothing new” answer, so short intervals stay cheap.

02 · Get a token

Mint an SDK token, exchange it for access

Programmatic access starts with an SDK token: a logged-in console user mints it once (Settings → SDK Tokens, admin-gated), you store it in WAGLE_API_TOKEN, and the SDK exchanges it for short-lived access tokens automatically. It is read-only - eval and sync only.

curl · Mint an SDK token
# Mint an SDK token: a durable, revocable REFRESH credential returned ONCE.
# Put it in WAGLE_API_TOKEN; the SDK exchanges it for short access tokens.
# User token, admin-gated. Omit "class" for backend trust; pass
# "TRUST_CLASS_FRONTEND" for a browser bundle that must never see a server_only flag.
curl -sS $WAGLE_URL/api.sdk.v1.SdkTokenService/CreateSdkToken \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{"project":"baseline","name":"web-app"}'
# -> {"token":"<refresh-credential, shown once>","models":{"sdkTokens":[...]},"graphs":[...]}
curl · Get a bearer token
# Programmatic access uses an SDK token: a durable, revocable REFRESH
# credential a logged-in console user mints once (CreateSdkToken — see below),
# stored in WAGLE_API_TOKEN. It is READ-ONLY: eval + sync only (SPEC D46).
# The SDK exchanges it for short 24h access tokens automatically; here is that
# exchange by hand — a PUBLIC call, the credential in the body is the auth.
curl -sS $WAGLE_URL/api.sdk.v1.SdkTokenService/RefreshSdkToken \
  -H 'Content-Type: application/json' \
  -d '{"refreshToken":"$WAGLE_API_TOKEN"}'
# -> {"accessToken":"<jwt, 24h>"}
# Pass that access token as Authorization: Bearer on eval / sync.
  • The SDK token is a durable, revocable refresh credential, returned once. It can never call an admin RPC - admin writes take a user session or an agent token.
  • Trust is a package choice, not a per-call flag: @wagleflags/sdk and the Go SDK are backend-trust; @wagleflags/sdk/frontend is the browser twin and never receives a server-only flag - pair it with a frontend-class token.
03 · Quickstart

Go SDK: seed once, evaluate locally

wagle.New blocks until the first seed lands and fails fast on a seed error, so the returned client is ready. Each evaluate is a pure, lock-free read of the held snapshot - zero network hop.

Go · Install + init
// go get github.com/unirakun/wagle-sdks/go
import wagle "github.com/unirakun/wagle-sdks/go"

// The SDK owns its transport: it dials https://api.wagle.sh over TLS and reads
// its bearer from WAGLE_API_TOKEN — the once-shown token from the console's
// "SDK Tokens" settings card. project is the opaque project UUID (D44), not its
// display name. New blocks until the first seed lands (fails fast on error),
// so the returned client is ready to Evaluate.
client, err := wagle.New("baseline", "production")
if err != nil {
	// initial seed failed
}
defer client.Close()

// Point at another server / pass the token explicitly / dial local h2c:
// client, err = wagle.New("baseline", "production",
// 	wagle.WithURL("$WAGLE_URL"),        // default https://api.wagle.sh
// 	wagle.WithToken(os.Getenv("MY_TOKEN")), // default WAGLE_API_TOKEN
// 	wagle.WithInsecure())                   // cleartext h2c for local dev
Go · Evaluate a flag
// A pure, local, lock-free read of the held snapshot — zero network hop.
// attrs may be nil (empty context).
res, err := client.Evaluate("new-checkout", "user-42", map[string]string{"plan": "pro"})
if err != nil {
	// a pinned eval error, e.g. flag not found
}
// res.VariationKey / res.Value / res.Reason / res.ValueType
04 · Quickstart

TypeScript SDK: backend, browser, React

createWagleClient is backend-trust. For an untrusted bundle use @wagleflags/sdk/frontend: the same seed, poll and local-eval loop, but a server-only flag never enters its store. Hooks take the client as an argument and re-render when a poll swaps the snapshot.

TypeScript · Install + init (backend-trust)
// pnpm add @wagleflags/sdk
import { createWagleClient } from "@wagleflags/sdk";

const client = createWagleClient({
  url: "$WAGLE_URL",
  project: "baseline",
  environment: "production",
  token: process.env.WAGLE_API_TOKEN, // the SDK refresh credential (SPEC D46)
});

await client.ready(); // resolves after the first seed

client.close(); // stop the pump when done
TypeScript · Evaluate a flag
// A pure, local read of the held snapshot — zero per-eval network hop.
const res = client.evaluate("new-checkout", {
  entityId: "user-42",
  attributes: { plan: "pro" },
});
// res is a Trace ({ variationKey, value, reason, ... })
// or an EvaluationError — narrow with isEvalError.
TypeScript · React hooks
import { useFlag } from "@wagleflags/sdk/react";

function Checkout({ client }: { client: WagleClient }) {
  // Re-renders live when a poll swaps the snapshot; evaluates locally.
  const value = useFlag(client, "new-checkout", {
    entityId: "user-42",
    attributes: { plan: "pro" },
  });
  return value === "true" ? <NewCheckout /> : <OldCheckout />;
}

// useFlag -> the served value string (undefined when unresolved);
// useEvaluate -> the full Trace | EvaluationError (reason, bucket, ...).
TypeScript · Browser bundle: the frontend-trust twin
// Untrusted client bundle (web/mobile)? Use the BROWSER-trust twin: the
// same seed -> poll -> local-eval loop, but a server_only flag never
// enters its store. Pair it with a frontend-class SDK token.
import { createWagleFrontendClient } from "@wagleflags/sdk/frontend";

const client = createWagleFrontendClient({
  url: "$WAGLE_URL",
  project: "baseline",
  environment: "production",
  token: "<frontend-sdk-token>",
});
05 · Stay fresh

The versioned poll: your freshness-vs-cost dial

After the seed the SDK re-checks sync.Get on an interval (default 30s), sending the version it holds: an unchanged poll is answered with a tiny response, so short intervals stay cheap. The interval is per client - 5–30s for interactive apps, 60–300s for batch.

Go · Staying fresh: configure the poll
// New seeds once from SyncService.Get, then re-checks on an interval.
// Each poll sends the held version; when nothing changed the server answers
// with a tiny "unchanged" response, so short intervals stay cheap. The
// interval is your freshness-vs-cost dial: 5-30s interactive, 60-300s batch.
client, err := wagle.New("baseline", "production",
	wagle.WithPollInterval(15*time.Second), // default 30s
)
if err != nil {
	// initial seed failed
}
defer client.Close()

v := client.Version() // the held (project, environment) config version
TypeScript · Staying fresh: configure the poll
// createWagleClient seeds once via sync.Get, then re-checks on an
// interval. Each poll sends the held version; when nothing changed the server
// answers with a tiny "unchanged" response, so short intervals stay cheap.
const client = createWagleClient({
  url: "$WAGLE_URL",
  project: "baseline",
  environment: "production",
  token: process.env.WAGLE_API_TOKEN,
  pollIntervalMs: 15_000, // the freshness-vs-cost dial (default 30_000)
});

await client.ready();
client.getSnapshot().version; // the held config version
Reference · Targeting

Segments: reusable targeting, resolved locally

A segment is a named predicate over context attributes (plan in [pro, enterprise]), reusable across flags. Segments ride down in the same snapshot as the flags, so a rule that targets one still evaluates in-process - no extra call.

curl · Create + reference a segment
# A segment is a reusable named predicate over context attributes. Conditions
# are ANDed; operators are OPERATOR_EQ / OPERATOR_IN here (no nesting).
curl -sS $WAGLE_URL/api.admin.v1.AdminService/CreateSegment \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{
    "project": "baseline",
    "environment": "production",
    "segment": {
      "key": "beta-testers",
      "conditions": [
        {"attribute": "plan", "op": "OPERATOR_IN", "values": ["pro", "enterprise"]}
      ]
    }
  }'
# -> {"models":{"segments":[...]},"graphs":[...],"version":"..."}

# Reference it from a flag rule — operator IN_SEGMENT, values = [segment key]:
#   "rules": [
#     {"conditions": [{"op": "OPERATOR_IN_SEGMENT", "values": ["beta-testers"]}],
#      "variationKey": "on"}
#   ]
Go · Segment targeting is local
// Segments arrive in the seed snapshot alongside the flags, so a rule that
// targets a segment (operator IN_SEGMENT) is resolved LOCALLY — zero extra
// network hop, same as any other rule. You just evaluate.
res, err := client.Evaluate("new-checkout", "user-42", map[string]string{"plan": "pro"})
if err != nil {
	// a pinned eval error
}
// When the entity's attributes place it in a targeted segment, res.Reason is
// the matched rule's Reason enum: TARGET_MATCH (fixed variation) or
// ROLLOUT (a percentage split).
  • Segments are one level deep on purpose - a segment can't reference another segment. Compose by ANDing segments in a rule.
Reference · HTTP API

The HTTP API: every RPC is plain JSON over POST

The backend speaks Connect over plain HTTP, so curl is a real client: every RPC is a POST of camelCase JSON to /api.<svc>.v1.<Service>/<Method> with Authorization: Bearer. Create a flag (admin - user token), evaluate, and seed, straight from a terminal.

curl · Create your first flag
curl -sS $WAGLE_URL/api.admin.v1.AdminService/CreateFlag \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{
    "project": "baseline",
    "environment": "production",
    "flag": {
      "key": "new-checkout",
      "valueType": "VALUE_TYPE_BOOL",
      "variations": [
        {"key": "on", "value": "true"},
        {"key": "off", "value": "false"}
      ],
      "environments": {
        "production": {"enabled": true, "defaultVariationKey": "on", "offVariationKey": "off"}
      }
    }
  }'
# -> {"models":{"flags":[...]},"graphs":[...],"version":"1"}
curl · Evaluate a flag
curl -sS $WAGLE_URL/api.eval.v1.EvaluationService/Evaluate \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{"project":"baseline","environment":"production","flagKey":"new-checkout","context":{"entityId":"user-42","attributes":{"plan":"pro"}}}'
# -> {"variationKey":"on","value":"true","reason":"REASON_DEFAULT","valueType":"VALUE_TYPE_BOOL"}
curl · Seed + efficient poll
# Initial seed: the whole (project, environment) snapshot + its version.
curl -sS $WAGLE_URL/api.sync.v1.SyncService/Get \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{"project":"baseline","environment":"production"}'
# -> {"models":{"flags":[...],"segments":[...]},"graphs":[...],"version":"1"}

# Efficient poll: send the version you hold — an unchanged poll returns a
# tiny no-change response instead of the whole snapshot.
curl -sS $WAGLE_URL/api.sync.v1.SyncService/Get \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{"project":"baseline","environment":"production","sinceVersion":"1"}'
# -> {"version":"1","unchanged":true}

# The SDKs poll this same Get on an interval (default 30s, tunable):
# the freshness-vs-cost dial. An unchanged poll costs almost nothing.
  • The example project is the placeholder baseline; your real project is an opaque UUID - discover it via ListProjects.
  • A uint64 (like a config version) serializes as a JSON string - that's protojson, not a quirk to work around.
Reference · Audit

Audit history: who changed what, exactly

Every write is atomic, bumps one per-project version and appends a history entry attributed to the authenticated actor. Read it back per project or per flag - each entry carries the full snapshot at that version, so consecutive entries diff cleanly.

curl · Read the audit timeline
# Project audit view: the latest version per entity, grouped by type.
curl -sS $WAGLE_URL/api.admin.v1.AdminService/GetHistory \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{"project":"baseline","environment":"production"}'
# -> {"history":[{"type":"HISTORY_TYPE_FLAG_VERSION","entries":{...}}, ...],"version":"..."}

# One flag generation's full write timeline (oldest first), fetched by the
# flag's immutable UUID (flagId — SPEC D54; ids come from ListFlags). Each entry
# carries the protojson snapshot at that version for consecutive-version diffs.
# ("key":"new-checkout" is the compat request instead: it resolves the NEWEST
# generation only — a recreated key never concatenates its predecessor's rows.)
curl -sS $WAGLE_URL/api.admin.v1.AdminService/GetFlagHistory \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{"project":"baseline","environment":"production","flagId":"<flag-uuid>"}'
# -> {"entries":[{"sourceId":"baseline/new-checkout","entityId":"<flag-uuid>",
#      "entityKey":"new-checkout","createdBy":"...","snapshot":"{...}"}, ...]}
# (protojson omits zero values: "deleted":true appears only on delete rows.)
Reference · Errors

Errors are pinned bytes, not prose

Errors are Connect JSON - a snake_case code, a message, a mapped HTTP status. Evaluate codes and messages are pinned, stable across releases: clients may depend on the exact bytes, and both SDKs surface the same code and message the wire does.

curl · Wire errors (pinned)
# Errors are Connect JSON — {"code":"<snake_case>","message":"..."} with a
# mapped HTTP status. Eval/sync/admin are authenticated, so the gate answers
# first: with NO bearer token you get 401 unauthenticated. This needs no
# credential — Run it:
curl -sS $WAGLE_URL/api.eval.v1.EvaluationService/Evaluate \
  -H 'Content-Type: application/json' \
  -d '{"project":"baseline","environment":"production","flagKey":"missing"}'
# -> HTTP 401 {"code":"unauthenticated","message":"missing bearer token"}

# Once authenticated (against your OWN project) the pinned Evaluate errors show —
# these messages are stable across releases, clients may depend on the bytes:
#   missing flag:   HTTP 404 {"code":"not_found","message":"flag not found: baseline/production/missing"}
#   no environment: HTTP 400 {"code":"invalid_argument","message":"environment is required"}
Reference · Agents

MCP: drive your flags from an AI agent

The admin surface is exposed to agents over a Streamable-HTTP MCP endpoint at /mcp - 15 tools covering flag and segment CRUD, rollouts, history and evaluation. Don't script tool calls: connect a host and ask in plain language.

MCP · Host config (.mcp.json)
{
  "mcpServers": {
    "wagle-admin": {
      "type": "http",
      "url": "$WAGLE_URL/mcp",
      "headers": { "Authorization": "Bearer <refresh-credential>" }
    }
  }
}
MCP · Prompts to try
# Claude Code: register the endpoint once, then just ask.
claude mcp add --transport http wagle-admin \
  $WAGLE_URL/mcp \
  --header "Authorization: Bearer <refresh-credential>"

# The agent discovers the tools itself - you talk product, it picks the tool:
> Which flags are enabled in production, and which are still fully off?
> Turn new-checkout off in production.
> Roll new-checkout out to 25% in production, everyone else stays off.
> Create a bool flag "maintenance-banner" in production, default off.
> Who changed new-checkout recently, and what exactly changed?
  • The host's bearer is the agent's long-lived refresh credential (minted in Settings → Agents) - the server exchanges it in-process for short access tokens, and revoking the agent cuts it off.
  • MCP is agent-only: a user token on the /mcp path is refused with permission_denied, so don't debug that blind.
Reference · FAQ

Frequently asked questions

Is there a REST API for feature flags?

Yes - every RPC is a plain POST of JSON over HTTP (the Connect protocol), so curl is a real client. No gRPC toolchain, no SDK required: create flags, evaluate, and sync against /api.<service>.v1 endpoints with a bearer token.

Does evaluating a flag make a network call?

No. The SDK seeds an in-memory snapshot once, then every evaluation is a local, in-process read - zero network hops, no database on the hot path. Local evaluations are free and unlimited; plans price sync volume, never evaluations.

How fast do flag changes reach my app?

On the next poll: each SDK re-checks on an interval you choose (default 30 seconds, tunable per client). Changes are eventually consistent - fast, but not an atomic fleet-wide flip in the same millisecond.

What happens if wagle is unreachable?

Your app keeps serving the last snapshot it saw - stale-but-available, reads never fail. Freshness resumes on the next successful poll. The cost: “turn this off now” is bounded by your poll interval.

Which SDKs does wagle have?

Go and TypeScript for backends, a browser-trust twin (@wagleflags/sdk/frontend) that never receives server-only flags, and React hooks. All evaluate byte-identically - one normative algorithm, pinned by a shared vector file in CI. No mobile SDKs today.

Can an AI agent manage my feature flags?

Yes. The admin surface is exposed over MCP (Streamable HTTP) at /mcp: 15 tools covering flag and segment CRUD, percentage rollouts, history and evaluation. Access is agent-only, authenticated by a revocable credential.

Mint a token and wire it live.

Sign in to create a project, mint an SDK token, and see these exact snippets interpolated with your live context in the console dev panel.