Identity and Attribution

For: Developers and operators configuring who can call the gateway and how costs are tracked.

Configuration Overview | Credentials | Workloads | Policies | Identity & Attribution


Every request that flows through Axemere Gateway carries two independent pieces of context: identity (who is making the request, for policy enforcement) and attribution (how to label the cost record, for billing and reporting). Understanding which fields serve which purpose, and how to populate them in each deployment style, is the key to getting governance and reporting right.

Table of Contents


The two dimensions: identity vs attribution

Attribution - how to label the cost

Identity - who is calling

org_id
tenant

workload_id
application or service

project_id
cost bucket / chargeback unit

account_id
billing account

customer_id
end customer

labels
free-form tags

Identity fields (org_id, workload_id) determine which policy rules apply. A request from wl-chatbot-prod may be allowed to call Anthropic; a request from wl-dev-sandbox may not. These fields gate access.

Attribution fields (project_id, account_id, customer_id, labels) determine how the request is recorded and who is charged. They do not affect whether a request is allowed, unless your policy explicitly requires them. They show up in execution records, spend reports, and the audit log.

The separation matters: two requests from the same workload can go to the same provider but charge to different projects (e.g. two different end customers of your SaaS product), and that's entirely normal.


Field reference

org_id

The top-level tenant identifier. Every resource (workloads, credentials, policies) is scoped to an org. In a self-hosted single-tenant deployment you have exactly one org. In a managed multi-tenant deployment, each customer organization is a separate org_id.

Who sets it: The gateway operator. Set once via MVGC_ORG_ID or the gateway config file. Can callers override it: No. In managed mode the gateway enforces a specific org_id and rejects requests that don't match. Format: Any string; conventionally a UUIDv7 or a slugified name like org-acme-corp.


workload_id

The identity of the calling application or service. Every registered workload has its own policy scope, risk scoring baseline, and default attribution. Think of it as a service account for your application.

Who sets it:

  • Explicit mode: the caller, in the JSON body.
  • Transparent proxy mode: X-MVGC-Workload-ID header → w/ path segment in the base URL → gateway DefaultWorkloadID config. First non-empty source wins.
  • Full MITM proxy: gateway DefaultWorkloadID config only.

One workload per app is the right mental model. If you run a customer chatbot, an internal analytics pipeline, and a developer sandbox, each is a separate workload. Individual users within an app are not separate workloads: they're caller_id or customer_id values.

Format: Any string; conventionally wl-{service}-{env} e.g. wl-chatbot-prod.


project_id

A billing or cost-allocation bucket. Use this to aggregate spend across multiple requests, workloads, or even multiple services that all belong to the same initiative. The gateway's spend reports group by project_id.

Who sets it:

  • Explicit mode: the caller, in the JSON body under attribution.project_id.
  • Proxy mode: the X-MVGC-Project-ID header, or the workload's default_attribution.project_id.

Typical uses:

  • A product team's cost center (proj-chatbot-v2)
  • A sprint or initiative (proj-q2-migration)
  • A per-customer billing bucket in a SaaS app (proj-customer-acme)
  • An environment (proj-production, proj-staging)

Format: Any string. Conventionally proj-{name}.


account_id

The billing account that should bear the cost. In simple deployments this mirrors project_id. In larger organizations it maps to a financial entity: a department code, a cost center number, or an external billing system reference.

Who sets it: Same as project_id: caller, header, or workload default.

Typical uses:

  • A finance department code (acct-eng-001)
  • A cloud billing account ID (acct-gcp-proj-12345)
  • An internal chargeback account for a business unit

Format: Any string. May be set to match an external billing reference.


customer_id

The end customer this request is on behalf of. This field is specifically for SaaS products and MSPs that run one gateway but serve multiple customers. It lets you break down costs and audit logs by customer without running a separate gateway per customer.

Who sets it: The application, typically extracted from the authenticated user session and injected as a header or included in the explicit request body.

Typical uses:

  • Your SaaS customer's identifier (cust-tenant-abc)
  • A reseller's end client (cust-megacorp-london)
  • An internal business unit in a large enterprise being billed separately

Format: Any string. Conventionally matches your own customer ID system.


labels

Arbitrary key-value pairs attached to every execution record. Labels are for anything that doesn't fit the structured fields above. They show up in the audit log and are available for reporting and filtering.

Who sets it: Workload default_attribution.labels, or per-request in explicit mode.

Typical uses:

  • env: prod / env: staging
  • team: platform / team: growth
  • feature: summarization / feature: code-review
  • region: us-east
  • release: v2.4.1

Format: map[string]string. Key and value are both arbitrary strings.


How fields are set — by ingress mode

Explicit mode

The caller sends a full ActionRequest JSON body to POST /v1/actions:execute. All fields are in the body. This gives the caller full control.

{
  "schema": "mvgc.action_request.v2",
  "org_id": "org-acme-corp",
  "workload_id": "wl-chatbot-prod",
  "attribution": {
    "project_id": "proj-chatbot-q3",
    "account_id": "acct-eng-001",
    "customer_id": "cust-tenant-abc",
    "labels": { "env": "prod", "feature": "summarization" }
  },
  "action": {
    "type": "ai.infer",
    "target_host": "api.anthropic.com",
    "params": { "model": "claude-3-5-sonnet-20241022" }
  }
}

Workload defaults fill any attribution fields the caller omits.


Transparent proxy — SDK base URL

The AI SDK is pointed at the gateway instead of the provider. The application code is unchanged. Example for Anthropic:

export ANTHROPIC_BASE_URL=http://gateway:7080/proxy/anthropic

The SDK sends POST /proxy/anthropic/v1/messages; the gateway strips /proxy/anthropic and forwards /v1/messages upstream. No code changes required.

How identity and attribution flow:

FieldSource (first match wins)
org_idX-MVGC-Org-ID header → MVGC_ORG_ID gateway config
workload_idX-MVGC-Workload-ID header → w/ path segment → MVGC_DEFAULT_WORKLOAD_ID gateway config
project_idX-MVGC-Project-ID header → p/ path segment → workload default_attribution.project_id
account_idX-MVGC-Account-ID header → a/ path segment → workload default_attribution.account_id
customer_idX-MVGC-Customer-ID header → c/ path segment → workload default_attribution.customer_id
labelsworkload default_attribution.labels

The X-MVGC-* headers and path attribution segments are stripped before the request is forwarded upstream; providers never see them.

When you have multiple workloads behind a single gateway, the preferred zero-code-change approach is to encode the workload in the base URL:

export ANTHROPIC_BASE_URL=http://gateway:7080/proxy/anthropic/w/wl-chatbot-prod

Alternatively, inject the X-MVGC-Workload-ID header in your application's HTTP client middleware. For per-customer attribution in a SaaS product, encode customer_id in the base URL (/c/{customer_id}) or inject X-MVGC-Customer-ID from the authenticated user session.

Priority and policy context: All four path-encodable fields follow the same waterfall: header beats path beats workload default. For project_id, account_id, and customer_id, overriding via header changes only that annotation field. For workload_id, overriding via header changes the entire policy context, since a different workload means a different policy bundle and different default_attribution. Keep this in mind when building middleware that injects X-MVGC-Workload-ID against a base URL that already encodes w/.


Transparent proxy — OS or sidecar proxy

The operating system or a service mesh sidecar is configured to route all HTTP egress through the gateway. The application code is completely unaware of the gateway.

# Linux/macOS
export HTTPS_PROXY=http://gateway:7080
export HTTP_PROXY=http://gateway:7080

In this mode X-MVGC-* headers must be injected by the proxy chain (e.g. Envoy filter, nginx header, or a thin wrapper around the outbound socket) because the application is not aware of the gateway. If no headers arrive, the gateway falls back to gateway-level defaults.

For teams running this mode, the most practical approach is:

  1. Set MVGC_DEFAULT_WORKLOAD_ID to the workload for this gateway instance.
  2. Configure default_attribution.project_id on that workload.
  3. Optionally run a sidecar that injects X-MVGC-Customer-ID from the running application's environment.

Full MITM proxy

The gateway intercepts HTTPS traffic via CONNECT tunneling and a CA certificate installed on client machines. This is the most transparent mode: no client SDK changes, no proxy env vars per-application.

In this mode there is no mechanism for per-request header injection unless you add a sidecar. All identity and attribution come from the gateway config:

FieldSource
org_idMVGC_ORG_ID gateway config
workload_idMVGC_DEFAULT_WORKLOAD_ID gateway config
Attributionworkload default_attribution.*

If multiple applications are behind the same MITM gateway and you need per-application workload attribution, run one gateway instance per application (each with its own MVGC_DEFAULT_WORKLOAD_ID), or switch those applications to SDK base URL mode.


The attribution fill-in waterfall

Attribution fields are filled in from the first source that provides a non-empty value:

1. Per-request      — JSON body (explicit mode) or X-MVGC-* headers (proxy mode)
2. Path-encoded     — w/ p/ a/ c/ segments in the proxy base URL (transparent proxy only)
3. Workload default — default_attribution configured on the workload definition
4. Gateway default  — MVGC_DEFAULT_WORKLOAD_ID gateway config (workload_id only)
5. Empty / omitted

This means you can encode workload_id and project_id once in the base URL and never pass them per-request, while still allowing callers to override them via header when needed (subject to policy constraints; see Policy implications).


Examples

Individual developer

Situation: You're building a personal project. One gateway, one AI provider, one of you.

You don't need any attribution fields. The gateway default workload handles everything. The only thing you need is a credential and a policy rule allowing your requests.

# configs/workloads/workloads.yaml
workloads:
  - workload_id: default
    org_id: org-my-project
    name: "My Project"
    allowed_connection_types:
      - sdk_redirect
# Point your SDK at the gateway — no code changes
export ANTHROPIC_BASE_URL=http://localhost:7080/proxy/anthropic

No attribution setup needed. Execution records are created; spend is visible in the console. If you later want to track costs by feature, add default_attribution.labels: {feature: chat} to the workload or start passing X-MVGC-Project-ID as you grow.


Small team with a shared API key

Situation: A 3-person team, one shared Anthropic key, want to see who's spending what.

Create one workload per team member (or per project). The gateway's spend report breaks down by workload.

workloads:
  - workload_id: wl-alice
    org_id: org-my-startup
    name: "Alice's tooling"
    default_attribution:
      project_id: proj-alice

  - workload_id: wl-bob
    org_id: org-my-startup
    name: "Bob's experiments"
    default_attribution:
      project_id: proj-bob

  - workload_id: wl-shared
    org_id: org-my-startup
    name: "Shared CI pipeline"
    default_attribution:
      project_id: proj-ci

Each person sets X-MVGC-Workload-ID in their SDK wrapper (one line of config). Spend reports show per-project breakdowns. A budget cap policy on proj-bob prevents runaway experiment costs.


Startup: multiple services, one cost center

Situation: Three backend services, each calling Anthropic, all charged to a single engineering budget.

One workload per service, one project_id for the whole team.

workloads:
  - workload_id: wl-chatbot
    org_id: org-acme
    name: "Customer Chatbot"
    default_attribution:
      project_id: proj-engineering
      labels:
        service: chatbot
        env: prod

  - workload_id: wl-summarizer
    org_id: org-acme
    name: "Document Summarizer"
    default_attribution:
      project_id: proj-engineering
      labels:
        service: summarizer
        env: prod

  - workload_id: wl-code-review
    org_id: org-acme
    name: "Code Review Assistant"
    default_attribution:
      project_id: proj-engineering
      labels:
        service: code-review
        env: prod

All three roll up to proj-engineering in spend reports, but the service label lets you see the breakdown by service. A single budget cap policy on proj-engineering enforces the total. Policy conditions on workload_id control which services can call which providers and models.


Enterprise: team-level chargeback

Situation: Engineering, Product, and Data Science teams each have a budget. Finance needs a monthly report per team. Each team has multiple services.

Use account_id to represent the team budget and project_id for the specific initiative.

workloads:
  - workload_id: wl-eng-api
    org_id: org-bigcorp
    name: "Engineering API Service"
    default_attribution:
      account_id: acct-engineering
      project_id: proj-eng-api-v3
      labels: { team: engineering, env: prod }

  - workload_id: wl-eng-infra
    org_id: org-bigcorp
    name: "Engineering Infra Automation"
    default_attribution:
      account_id: acct-engineering
      project_id: proj-eng-infra
      labels: { team: engineering, env: prod }

  - workload_id: wl-product-analytics
    org_id: org-bigcorp
    name: "Product Analytics"
    default_attribution:
      account_id: acct-product
      project_id: proj-product-insights
      labels: { team: product, env: prod }

  - workload_id: wl-ds-models
    org_id: org-bigcorp
    name: "Data Science Model Evaluation"
    default_attribution:
      account_id: acct-data-science
      project_id: proj-ds-eval
      labels: { team: data-science, env: prod }

Finance queries spend grouped by account_id each month. Each team's lead sees their own project_id breakdown. Budget caps are set at the account_id level in policy, so each team is independent.


Enterprise: department budget caps

Situation: Same as above, but each team has a hard cap of $5,000/month and you want policy to enforce it.

Add a budget policy layer per account:

# policy rules (budgets layer)
- id: budget-engineering
  conditions:
    - field: context.attribution.account_id
      equals: acct-engineering
  effect:
    budget:
      usd_per_day: 166.67   # ~$5000/month
  decision: deny
  reason: "engineering monthly budget exceeded"

- id: budget-product
  conditions:
    - field: context.attribution.account_id
      equals: acct-product
  effect:
    budget:
      usd_per_day: 83.33    # ~$2500/month
  decision: deny
  reason: "product monthly budget exceeded"

Each team's requests are denied once their daily budget is exhausted. The gateway's GetOrgSpendToday call returns blocked_requests for the NOC dashboard.


SaaS: per-customer cost isolation

Situation: You run a SaaS product. Each of your customers is a tenant. You want to see AI spend per customer, set per-customer limits, and attribute costs back for billing.

Your application knows the logged-in customer. Inject customer_id per request.

Explicit mode (most control):

import httpx

def call_ai(prompt: str, customer_id: str) -> str:
    resp = httpx.post("http://gateway:7080/v1/actions:execute",
        headers={"MVGC-Admin-Token": token},
        json={
            "schema": "mvgc.action_request.v2",
            "org_id": "org-my-saas",
            "workload_id": "wl-saas-backend",
            "attribution": {
                "customer_id": customer_id,
                "project_id": f"proj-customer-{customer_id}",
            },
            "action": {
                "type": "ai.infer",
                "target_host": "api.anthropic.com",
                "params": {"model": "claude-3-5-sonnet-20241022"}
            }
        })
    return resp.json()["result"]["body"]

Transparent proxy mode (zero application changes — customer_id in the base URL):

import anthropic

def make_client(customer_id: str) -> anthropic.Anthropic:
    """One client instance per customer — attribution is encoded in the base URL."""
    return anthropic.Anthropic(
        base_url=f"http://gateway:7080/proxy/anthropic/w/wl-saas-backend/c/{customer_id}",
        api_key="any-value",
    )

# No middleware, no per-request headers — just one client per customer
acme_client   = make_client("cust-acme-corp")
globex_client = make_client("cust-globex-ind")

If you need per-request customer_id within a single shared client, inject X-MVGC-Customer-ID as a header; it overrides the path value for that request only.

Policy: per-customer budget cap:

# Deny any customer who exceeds $50/day
- id: per-customer-budget
  conditions:
    - field: context.attribution.customer_id
      exists: true
  effect:
    budget:
      usd_per_day: 50.00
      group_by: customer_id
  decision: deny
  reason: "customer daily AI budget exceeded"

Spend reports let you see exactly which customer is driving cost. If customer-abc is using 10× the expected amount, you can see it immediately and act.


MSP: managing AI spend for multiple clients

Situation: You are a managed service provider with 20 clients. Each client has a contract with a different AI spend allowance. Some use OpenAI, some Anthropic.

Run one gateway, with one workload per client. Each client gets their own customer_id and budget cap.

workloads:
  - workload_id: wl-client-acme
    org_id: org-my-msp
    name: "ACME Corp"
    default_attribution:
      customer_id: cust-acme
      project_id: proj-acme
      account_id: acct-acme

  - workload_id: wl-client-globex
    org_id: org-my-msp
    name: "Globex Industries"
    default_attribution:
      customer_id: cust-globex
      project_id: proj-globex
      account_id: acct-globex

Each client is given an API key scoped to their workload. The gateway's managed API key authentication ensures each client can only use their own workload. Policy rules can restrict which models or providers each client may access, and per-customer budget caps enforce contract limits automatically.

For deployments where clients use an AI SDK directly, encode both workload_id and customer_id in the base URL so attribution flows without any application code:

# Client ACME — their SDK is configured with this base URL
ANTHROPIC_BASE_URL=http://gateway:7080/proxy/anthropic/w/wl-client-acme/c/cust-acme

# Client Globex
ANTHROPIC_BASE_URL=http://gateway:7080/proxy/anthropic/w/wl-client-globex/c/cust-globex

Agent framework: multi-step workflows

Situation: An orchestrator agent dispatches tasks to specialized sub-agents (summarizer, coder, planner). You want the audit log to show which step of a workflow generated which cost.

Use labels to tag the agent role and project_id for the overall workflow.

Explicit mode in Python:

from typing import Literal

AgentRole = Literal["orchestrator", "summarizer", "coder", "planner"]

def agent_call(prompt: str, workflow_id: str, role: AgentRole) -> str:
    resp = httpx.post("http://gateway:7080/v1/actions:execute",
        json={
            "schema": "mvgc.action_request.v2",
            "org_id": "org-acme",
            "workload_id": "wl-agent-framework",
            "attribution": {
                "project_id": f"proj-workflow-{workflow_id}",
                "labels": {
                    "agent_role": role,
                    "workflow_id": workflow_id,
                }
            },
            "action": {
                "type": "ai.infer",
                "target_host": "api.anthropic.com",
                "params": {"model": "claude-3-5-sonnet-20241022"}
            }
        })
    return resp.json()["result"]["body"]

# Each step tags itself
plan   = agent_call(task, wf_id, "planner")
code   = agent_call(plan, wf_id, "coder")
review = agent_call(code, wf_id, "summarizer")

Execution records for the entire workflow share proj-workflow-{id}, so spend reports show total workflow cost. The agent_role label breaks it down by step. If planning costs more than coding, you see it.

Transparent proxy mode — per-workflow cost tracking without code changes:

import anthropic

def make_workflow_client(workflow_id: str) -> anthropic.Anthropic:
    return anthropic.Anthropic(
        base_url=f"http://gateway:7080/proxy/anthropic/w/wl-agents/p/proj-wf-{workflow_id}",
        api_key="any-value",
    )

client = make_workflow_client("run-20240407-abc123")
# All steps share proj-wf-run-20240407-abc123 — total workflow cost visible in reports

Per-step label granularity: agent_role and other per-step labels cannot be path-encoded, since labels are map[string]string and are not supported in the path. Use explicit mode (shown above) when you need per-step cost breakdown in addition to per-workflow totals.


Policy implications

Attribution fields can be required, restricted, or used in budget conditions. The most common patterns:

Require a project before allowing any request:

- id: require-project
  conditions: []   # matches all requests
  effect:
    enforce_attribution:
      require: [project_id]
  decision: require_attribution
  reason: "all requests must carry a project_id for cost allocation"

Allow a workload only when serving a known customer:

- id: saas-customer-only
  conditions:
    - field: context.workload_id
      equals: wl-saas-backend
    - field: context.attribution.customer_id
      exists: false
  effect:
    decision: deny
  reason: "wl-saas-backend must include customer_id on every request"

Budget cap scoped to a label value:

- id: prod-budget-cap
  conditions:
    - field: context.attribution.labels.env
      equals: prod
  effect:
    budget:
      usd_per_day: 500.00
  decision: deny
  reason: "production environment daily budget exceeded"

Proxy modes: setting fields without headers

If you use transparent or MITM proxy mode and cannot inject headers (e.g. legacy applications, third-party tools), the cleanest solution is to configure attribution directly on the workload:

# configs/workloads/workloads.yaml
workloads:
  - workload_id: wl-legacy-etl
    org_id: org-bigcorp
    name: "Legacy ETL Job (MITM proxy)"
    default_attribution:
      project_id: proj-data-engineering
      account_id: acct-data-eng
      labels:
        service: legacy-etl
        env: prod

Every request through this gateway instance inherits these values automatically: no header injection, no application changes.

For the /proxy/{provider} SDK mode, encode attribution directly in the base URL using path segments; no header injection required:

# workload + project in the base URL; no application code changes needed
export ANTHROPIC_BASE_URL=http://gateway:7080/proxy/anthropic/w/wl-legacy-etl/p/proj-data-engineering

See the path segment table in the Transparent proxy — SDK base URL section for the full URL format and supported fields.


See also

  • Workloads — registering workloads and configuring default_attribution
  • Policiesenforce_attribution, budget rules, and identity conditions
  • Configuration Overview — how workloads, credentials, and policies interrelate
  • Glossary — definitions for all identity and attribution fields
  • Developer Integration Guide — explicit mode request schema, delegation tokens