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.
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.
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.
# 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.# 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>"}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 -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 -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"}# 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.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.
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()// 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.ValueTypecreateWagleClient 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.
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// 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" }),
});// 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.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, ...).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.
// 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// 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 versionErrors 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.
# 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"}The admin surface is exposed to agents over a Streamable-HTTP MCP endpoint at /mcp. Twelve tools map 1:1 onto AdminService.
{
"mcpServers": {
"wagle-admin": {
"type": "http",
"url": "$WAGLE_URL/mcp",
"headers": { "Authorization": "Bearer <refresh-credential>" }
}
}
}# 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_historySign in to create a project, mint an access token, and see these exact snippets interpolated with your live context in the console dev panel.