Docs · API, SDKs, MCP

Wire your first flag.

The backend speaks Connect over plain HTTP, so the curl examples below are real requests — set a base URL and token in Connection and Run them in the browser. Then seed an SDK once and evaluate locally, with no per-eval network hop.

Connection

Point the examples at your server.

Set your base URL and a bearer token; the Run buttons below POST the Connect request straight to it. No token yet? Run Get a token under Auth and adopt the one it mints.

Set a base URL to Run.

Your token stays in this tab (sessionStorage) and is sent only to the base URL you set — it is never written into the copyable curl text, which keeps the $WAGLE_TOKEN placeholder so no secret is printed on the page.

Auth · principals

Every call is authenticated.

A request carries a bearer JWT (the console also accepts an httpOnly session cookie), resolving to a user or an agent. Run Get a token to mint one and adopt it for the calls below.

curl · Get a bearer token
# Sign up (Login has the same shape) — mints a 24h bearer token.
curl -sS $WAGLE_URL/api.auth.v1.AuthService/Signup \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","password":"your-password","displayName":"You"}'
# -> {"accessToken":"<jwt>","models":{"users":[...]}}
# Signup also provisions a starter project you administer.
curl · Agent credential flow
# 1) Create an agent principal (user token, admin-gated). BOTH secrets are
#    returned ONCE: a 24h access token + the durable refresh credential.
curl -sS $WAGLE_URL/api.agent.v1.AgentService/CreateAgent \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $WAGLE_TOKEN" \
  -d '{"project":"baseline","name":"ci-bot","role":"editor"}'
# -> {"accessToken":"<jwt, 24h>","models":{"agents":[...]},"graphs":[...],
#     "refreshToken":"<refresh-credential>"}

# 2) Exchange the refresh credential for a fresh access token whenever one
#    expires (public RPC — the credential IS the authentication; a revoked
#    one -> unauthenticated).
curl -sS $WAGLE_URL/api.agent.v1.AgentService/RefreshAgentToken \
  -H 'Content-Type: application/json' \
  -d '{"refreshToken":"<refresh-credential>"}'
# -> {"accessToken":"<jwt, 24h>"}
  • Users hold per-project roles (admin / editor / viewer); Signup provisions a starter project you administer.
  • Agents authenticate with a durable, revocable refresh credential they exchange for a short 24h access token — see the flow below.
  • Trust is a package choice, not a per-call flag: the Go SDK and @wagle/sdk are backend-trust; @wagle/sdk/frontend is the browser twin and never receives a server-only flag.
Try it · curl

Connect over HTTP: curl is a real client.

Every RPC is a plain POST of JSON to /api.<svc>.v1.<Service>/<Method> with camelCase fields and Authorization: Bearer. Run create → evaluate → seed top to bottom.

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.
  • Run retargets the example project onto your token's real one, so a just-signed-up account evaluates against the project it actually owns.
  • A uint64 (like a version) serializes as a JSON string. The SDKs poll this same Get on an interval you choose; an unchanged poll is a tiny response.
Quickstart · Go

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
import (
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	wagle "github.com/fabienjuif/wagle/sdk"
)

conn, err := grpc.NewClient("$WAGLE_URL", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
	// handle
}

// New blocks until the first seed lands (fails fast on a seed error),
// so the returned client is ready to Evaluate.
client, err := wagle.New("baseline", "production", conn)
if err != nil {
	// initial seed failed
}
defer client.Close()
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
Quickstart · TypeScript

TypeScript SDK, browser twin, React hooks.

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

TypeScript · Install + init (backend-trust)
import { createConnectTransport } from "@connectrpc/connect-web";
import { createWagleClient } from "@wagle/sdk";

const client = createWagleClient({
  project: "baseline",
  environment: "production",
  transport: createConnectTransport({ baseUrl: "$WAGLE_URL" }),
});

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

client.close(); // stop the pump when done
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 credential.
import { createWagleFrontendClient } from "@wagle/sdk/frontend";

const client = createWagleFrontendClient({
  project: "baseline",
  environment: "production",
  transport: createConnectTransport({ baseUrl: "$WAGLE_URL" }),
});
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 "@wagle/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, ...).
Staying fresh

Seed once, then poll on your interval.

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, your freshness-vs-cost dial.

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", conn,
	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({
  project: "baseline",
  environment: "production",
  transport,
  pollIntervalMs: 15_000, // the freshness-vs-cost dial (default 30_000)
});

await client.ready();
client.getSnapshot().version; // the held config version
Errors

Pinned codes and messages.

Errors are Connect JSON — a snake_case code, a message and 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. Run it to see a 404.

curl · Wire errors (pinned)
# Errors are Connect JSON — {"code":"<snake_case>","message":"..."} with a
# mapped HTTP status. Evaluate messages are PINNED, stable across
# releases: clients may depend on the exact bytes.
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":"missing"}'
# -> HTTP 404 {"code":"not_found","message":"flag not found: baseline/production/missing"}

# Omit environment:
# -> HTTP 400 {"code":"invalid_argument","message":"environment is required"}
MCP · agents

An MCP server on the same port.

The admin surface is exposed to agents over a Streamable-HTTP MCP endpoint at /mcp. Twelve tools map 1:1 onto AdminService.

MCP · Host config (.mcp.json)
{
  "mcpServers": {
    "wagle-admin": {
      "type": "http",
      "url": "$WAGLE_URL/mcp",
      "headers": { "Authorization": "Bearer <refresh-credential>" }
    }
  }
}
MCP · Register + toolset
# Claude Code: point it at the /mcp endpoint on the running server.
claude mcp add --transport http wagle-admin \
  $WAGLE_URL/mcp \
  --header "Authorization: Bearer <refresh-credential>"

# Twelve tools, 1:1 on AdminService:
#   list_projects, list_environments,
#   list_flags, get_flag, set_flag_state, create_flag, update_flag, delete_flag,
#   list_segments, get_segment, delete_segment,
#   get_flag_history
  • The host's bearer is the agent's long-lived refresh credential — the server exchanges it in-process for an access token, so a generic host never refreshes.
  • MCP is agent-only: a user token on the MCP path is refused with permission_denied, so don't debug that blind.

Mint a token and wire it live.

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