Delegation Tokens
For: Application developers and platform engineers integrating with Axemere Gateway.
Developer Integration | Delegation Tokens | Merkle Proof Verification
A delegation token grants a downstream caller scoped, time-limited authority to perform actions through Axemere Gateway. Tokens are Ed25519-signed JSON documents; the gateway verifies the signature and scope before allowing the request to proceed. No token, no action.
Table of Contents
- When to Use Delegation Tokens
- Token Structure
- Trust Model
- Creating a Token
- Using a Token in a Request
- Gateway Verification Steps
- Policy DSL Fields
- Delegation Chains
- Security Considerations
- Environment Variables
- Troubleshooting
- See Also
When to Use Delegation Tokens
Use delegation tokens when a system component needs to call an AI provider on behalf of a user or another service, but should not have full platform authority. Common patterns:
| Pattern | Without delegation | With delegation |
|---|---|---|
| AI agent calling OpenAI on behalf of a user | Agent uses platform credentials directly; all requests look the same in audit logs | Token embeds principal_id and project_id; each user's requests are attributed separately |
| Multi-tenant SaaS routing user requests | All spend aggregated under one workload | Per-user usd_max caps prevent one user from exhausting the budget |
| Orchestrator spawning sub-agents | Sub-agents have unconstrained action scope | Token restricts actions_allow and targets_allow; sub-agents cannot exceed their mandate |
Delegation tokens are optional. Requests without tokens are evaluated against policy with no delegation context. See Developer Integration for the base request format.
Token Structure
The wire schema is "mvgc.delegation.v2". All fields use JCS (RFC 8785) canonical serialization before signing.
{ "schema": "mvgc.delegation.v2", "delegation_id": "01955f3e-1234-7abc-8def-000000000001", "org_id": "01955f3e-0000-7abc-8def-000000000001", "workload_id": "01955f3e-0000-7abc-8def-000000000002", "principal_id": "user-abc", "policy_bundle_id": "01955f3e-0000-7abc-8def-000000000003", "issued_at": "2026-03-12T10:00:00Z", "expires_at": "2026-03-12T11:00:00Z", "budget": { "model_tokens_max": 50000, "usd_max": "5.00" }, "scope": { "actions_allow": ["ai.infer"], "targets_allow": ["api.openai.com"], "methods_allow": ["POST"], "resource_patterns_allow": ["^/v1/chat/.*"] }, "attribution": { "defaults": { "customer_id": "cust-42", "project_id": "proj-123", "labels": { "env": "prod" } }, "allow_overrides": ["project_id", "labels"] }, "jti": "unique-per-token-nonce", "sig": { "alg": "ed25519", "kid": "kid_cp_del_1", "sig": "<base64url>" } }
| Field | Required | Description |
|---|---|---|
schema | Yes | Must be "mvgc.delegation.v2" |
delegation_id | Yes | UUIDv7, unique token identifier |
org_id | Yes | Must match the gateway's configured org |
workload_id | Yes | Restricts this token to a specific workload |
principal_id | No | End-user or service identity (flows into attribution) |
policy_bundle_id | No | Pins evaluation to a specific bundle version |
issued_at | Yes | RFC 3339 issuance time |
expires_at | Yes | RFC 3339 expiry time |
budget | No | Spend and token caps |
scope | No | Action, target, method, and resource restrictions |
attribution | No | Attribution defaults and override permissions |
jti | Yes | Unique nonce, prevents token replay |
sig | Yes | Ed25519 signature envelope |
Scope Fields
All scope fields are lists of permitted values. An empty list or omitted field means no restriction for that dimension.
| Field | Description | Example |
|---|---|---|
actions_allow | Permitted action types | ["ai.infer", "http.request"] |
targets_allow | Permitted target hostnames | ["api.openai.com"] |
methods_allow | Permitted HTTP methods | ["POST"] |
resource_patterns_allow | Permitted URL path patterns (Go regex) | ["^/v1/chat/.*"] |
The gateway intersects the token's scope with policy. A request must satisfy both.
Budget Fields
| Field | Description |
|---|---|
model_tokens_max | Maximum total model tokens (input + output) for this token's lifetime |
usd_max | Maximum spend cap as a decimal string (e.g. "5.00") |
Budgets are enforced per-token in addition to workload-level budgets. When either cap is reached, subsequent requests using the token are denied.
Attribution Fields
attribution.defaults provides fallback values for fields not set on the request. attribution.allow_overrides lists which fields the caller may override per-request.
"attribution": { "defaults": { "customer_id": "cust-42", "project_id": "proj-123", "account_id": "acct-7", "labels": { "team": "payments", "env": "prod" } }, "allow_overrides": ["project_id", "labels"] }
A field in allow_overrides may be set by the caller in the request body. A field not in the list is locked to the token default; the caller cannot change it.
Trust Model
The token issuer holds the private signing key. The gateway holds only the public verification key. The gateway never generates tokens; it only verifies them.
Trust boundaries:
- The issuer is responsible for setting correct scope, budget, and expiry
- The gateway enforces that the token is valid and matches the request
- Policy may further restrict what a valid token can do
Creating a Token
Go SDK
import ( "github.com/axemere/gateway/delegation" "github.com/axemere/gateway/signing" "time" ) signer := signing.NewSigner(privKey) tokenBytes, err := delegation.CreateToken(signer, delegation.DelegationClaims{ IssuedBy: "my-backend-service", OrgID: "01955f3e-0000-7abc-8def-000000000001", WorkloadID: "01955f3e-0000-7abc-8def-000000000002", PrincipalID: "user-abc", // optional TTL: time.Hour, Scope: delegation.Scope{ ActionsAllow: []string{"ai.infer"}, TargetsAllow: []string{"api.openai.com"}, MethodsAllow: []string{"POST"}, ResourcePatternsAllow: []string{"^/v1/chat/.*"}, }, Budget: delegation.Budget{ USDMax: "5.00", ModelTokensMax: 50000, }, Attribution: delegation.TokenAttribution{ Defaults: map[string]string{ "project_id": "proj-123", "customer_id": "cust-42", }, AllowOverrides: []string{"project_id"}, }, }) if err != nil { return fmt.Errorf("create delegation token: %w", err) } // tokenBytes is the base64-encoded signed token for inclusion in requests
Raw JSON (any language)
- Build the token JSON document matching the schema above.
- Serialize to canonical JSON using JCS (RFC 8785): deterministic key order, no insignificant whitespace.
- Compute
SHA-256(canonical_bytes). - Sign the hash with Ed25519 using your delegation signing key.
- Base64url-encode the signature and embed it in the
sigenvelope. - Base64-encode the entire signed JSON for transport.
Using a Token in a Request
Include the token in the Action Request body:
{ "schema": "mvgc.action_request.v2", "request_id": "01955f3e-aaaa-7abc-8def-000000000099", "org_id": "01955f3e-0000-7abc-8def-000000000001", "caller_id": "my-backend-service", "delegation_id": "01955f3e-1234-7abc-8def-000000000001", "delegation_token": "<base64-encoded signed token>", "action": { "type": "ai.infer", "target": "https://api.openai.com", "method": "POST", "path": "/v1/chat/completions" } }
Or via HTTP headers when using transparent proxy mode:
X-MVGC-Delegation-ID: 01955f3e-1234-7abc-8def-000000000001
X-MVGC-Delegation-Token: <base64-encoded signed token>
Gateway Verification Steps
When a token is present, the gateway performs these checks in order before policy evaluation:
- Signature: Verified against
MVGC_DELEGATION_VERIFY_KEYusingkidfrom the token'ssigenvelope. - Expiry:
expires_atmust be in the future;issued_atmust not be more than 5 minutes in the future. - Org match: Token
org_idmust match the gateway's configured org. - Chain depth: The delegation chain depth must be ≤ 3 (see Delegation Chains).
- Scope intersection: At policy evaluation time, the request action and target are checked against the token's
scopefields.
Policy DSL Fields
After a token passes verification, these fields are available in policy rules:
| DSL field | Source | Description |
|---|---|---|
context.delegation.issued_by | principal_id / issuer | Identity of the token issuer |
context.delegation.principal_id | principal_id | End-user or service identity |
context.delegation.token_id | delegation_id | Unique token ID |
context.delegation.depth | chain depth | Integer string; max is "3" |
context.delegation.policy_bundle_id | policy_bundle_id | Bundle pinned by the token |
context.delegation.max_cost_usd | budget.usd_max | Per-token spend cap |
context.delegation.actions_allow | scope.actions_allow | Comma-joined list |
context.delegation.targets_allow | scope.targets_allow | Comma-joined list |
Use the in operator to check membership in list fields:
rules: - id: allow.delegation.infer.only conditions: all: - field: context.delegation.actions_allow operator: in value: "ai.infer" effect: allow
Delegation Chains
A token may itself authorize a downstream actor to further delegate. The depth field tracks how many times authority has been re-delegated:
- Maximum chain depth is 3.
- Each delegation hop increments the depth counter.
- A token with
depth=3cannot be re-delegated; any attempt results in a403 delegation_depth_exceeded. - Depth is enforced at verification time; it does not affect the token schema.
Security Considerations
| Risk | Mitigation |
|---|---|
| Token replay | jti (unique nonce); gateways reject duplicate delegation_id values within a sliding window |
| Token theft | Short TTL (minutes to hours); narrow scope.targets_allow limits blast radius |
| Overly broad scope | Set actions_allow and targets_allow to the minimum needed; test with policy trace |
| Signing key exposure | Store the signing private key in a secrets manager; rotate it if compromised |
| Chain explosion | Maximum depth of 3 is hardcoded and not configurable |
Token TTL guidance: Use the shortest TTL that does not cause operational friction. For user-session-scoped tokens, 1 hour is typical. For short-lived agent tasks, 5–15 minutes is preferred.
Environment Variables
| Variable | Description |
|---|---|
MVGC_DELEGATION_SIGN_KEY | Path to PEM-encoded Ed25519 private key used to sign tokens (issuer side) |
MVGC_DELEGATION_VERIFY_KEY | Path to PEM-encoded Ed25519 public key used to verify tokens (gateway side) |
The signing and verification keys are separate: the issuer system holds the private key; the gateway holds only the public key.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
403 invalid_delegation_token | Signature verification failed | Verify the signing key matches MVGC_DELEGATION_VERIFY_KEY; check base64 encoding |
403 delegation_token_expired | Token expires_at in the past | Issue a new token; check for clock skew between issuer and gateway |
403 org_id mismatch | Token org_id does not match gateway org_id | Ensure the token is issued for the correct org |
403 delegation_depth_exceeded | Chain depth exceeds 3 | Flatten the delegation chain; avoid chains deeper than 3 |
403 scope_violation | Action or target not in scope.actions_allow / scope.targets_allow | Widen token scope or narrow the request |
| Token works in test, denied in prod | Different MVGC_DELEGATION_VERIFY_KEY | Verify the prod gateway is configured with the correct public key |
See Also
- Developer Integration Guide: base request format and ingress modes
- Admin API Reference: workload and credential management
- Glossary: delegation_token
- Glossary: principal_id