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

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:

PatternWithout delegationWith delegation
AI agent calling OpenAI on behalf of a userAgent uses platform credentials directly; all requests look the same in audit logsToken embeds principal_id and project_id; each user's requests are attributed separately
Multi-tenant SaaS routing user requestsAll spend aggregated under one workloadPer-user usd_max caps prevent one user from exhausting the budget
Orchestrator spawning sub-agentsSub-agents have unconstrained action scopeToken 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>" }
}
FieldRequiredDescription
schemaYesMust be "mvgc.delegation.v2"
delegation_idYesUUIDv7, unique token identifier
org_idYesMust match the gateway's configured org
workload_idYesRestricts this token to a specific workload
principal_idNoEnd-user or service identity (flows into attribution)
policy_bundle_idNoPins evaluation to a specific bundle version
issued_atYesRFC 3339 issuance time
expires_atYesRFC 3339 expiry time
budgetNoSpend and token caps
scopeNoAction, target, method, and resource restrictions
attributionNoAttribution defaults and override permissions
jtiYesUnique nonce, prevents token replay
sigYesEd25519 signature envelope

Scope Fields

All scope fields are lists of permitted values. An empty list or omitted field means no restriction for that dimension.

FieldDescriptionExample
actions_allowPermitted action types["ai.infer", "http.request"]
targets_allowPermitted target hostnames["api.openai.com"]
methods_allowPermitted HTTP methods["POST"]
resource_patterns_allowPermitted URL path patterns (Go regex)["^/v1/chat/.*"]

The gateway intersects the token's scope with policy. A request must satisfy both.

Budget Fields

FieldDescription
model_tokens_maxMaximum total model tokens (input + output) for this token's lifetime
usd_maxMaximum 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

Signs token

Included in request

Verify sig + expiry + org_id

allow

deny

Token Issuer
(signing key: MVGC_DELEGATION_SIGN_KEY)

Delegation Token
Ed25519 signed

Gateway
(verify key: MVGC_DELEGATION_VERIFY_KEY)

Policy Engine

Action Execution

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)

  1. Build the token JSON document matching the schema above.
  2. Serialize to canonical JSON using JCS (RFC 8785): deterministic key order, no insignificant whitespace.
  3. Compute SHA-256(canonical_bytes).
  4. Sign the hash with Ed25519 using your delegation signing key.
  5. Base64url-encode the signature and embed it in the sig envelope.
  6. 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:

fail

ok

expired

ok

mismatch

ok

exceeded

ok

Token received

Verify Ed25519 signature
against MVGC_DELEGATION_VERIFY_KEY

Deny 403
invalid_delegation_token

Check expires_at not passed
issued_at not far future

Deny 403
delegation_token_expired

Verify org_id matches
gateway org_id

Deny 403
org_id mismatch

Check chain depth <= 3

Deny 403
delegation_depth_exceeded

Policy evaluation
with delegation context

  1. Signature: Verified against MVGC_DELEGATION_VERIFY_KEY using kid from the token's sig envelope.
  2. Expiry: expires_at must be in the future; issued_at must not be more than 5 minutes in the future.
  3. Org match: Token org_id must match the gateway's configured org.
  4. Chain depth: The delegation chain depth must be ≤ 3 (see Delegation Chains).
  5. Scope intersection: At policy evaluation time, the request action and target are checked against the token's scope fields.

Policy DSL Fields

After a token passes verification, these fields are available in policy rules:

DSL fieldSourceDescription
context.delegation.issued_byprincipal_id / issuerIdentity of the token issuer
context.delegation.principal_idprincipal_idEnd-user or service identity
context.delegation.token_iddelegation_idUnique token ID
context.delegation.depthchain depthInteger string; max is "3"
context.delegation.policy_bundle_idpolicy_bundle_idBundle pinned by the token
context.delegation.max_cost_usdbudget.usd_maxPer-token spend cap
context.delegation.actions_allowscope.actions_allowComma-joined list
context.delegation.targets_allowscope.targets_allowComma-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:

issues token
depth=1

issues token
depth=2

issues token
depth=3

cannot delegate further

Platform
depth=0

Agent Service
depth=1

Worker
depth=2

Sub-agent
depth=3 (max)

denied

  • Maximum chain depth is 3.
  • Each delegation hop increments the depth counter.
  • A token with depth=3 cannot be re-delegated; any attempt results in a 403 delegation_depth_exceeded.
  • Depth is enforced at verification time; it does not affect the token schema.

Security Considerations

RiskMitigation
Token replayjti (unique nonce); gateways reject duplicate delegation_id values within a sliding window
Token theftShort TTL (minutes to hours); narrow scope.targets_allow limits blast radius
Overly broad scopeSet actions_allow and targets_allow to the minimum needed; test with policy trace
Signing key exposureStore the signing private key in a secrets manager; rotate it if compromised
Chain explosionMaximum 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

VariableDescription
MVGC_DELEGATION_SIGN_KEYPath to PEM-encoded Ed25519 private key used to sign tokens (issuer side)
MVGC_DELEGATION_VERIFY_KEYPath 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

SymptomLikely causeFix
403 invalid_delegation_tokenSignature verification failedVerify the signing key matches MVGC_DELEGATION_VERIFY_KEY; check base64 encoding
403 delegation_token_expiredToken expires_at in the pastIssue a new token; check for clock skew between issuer and gateway
403 org_id mismatchToken org_id does not match gateway org_idEnsure the token is issued for the correct org
403 delegation_depth_exceededChain depth exceeds 3Flatten the delegation chain; avoid chains deeper than 3
403 scope_violationAction or target not in scope.actions_allow / scope.targets_allowWiden token scope or narrow the request
Token works in test, denied in prodDifferent MVGC_DELEGATION_VERIFY_KEYVerify the prod gateway is configured with the correct public key

See Also