Overview
Bifrost Enterprise already answers who is calling. Virtual keys, teams, customers, business units and access profiles are all attributes of the caller: they are fixed the moment a request authenticates, and spend flows up one ownership tree. A project answers a different question: what is this call for? A project is a per-request access gate and accounting scope, activated by a header. It sits deliberately outside the ownership hierarchy. A caller keeps their own identity and their own access; naming a project re-shapes what that one request may reach and where its spend lands. A project never authenticates anything. It is not a credential, and it does not replace the caller. It composes with an already-authenticated principal, which is what keeps it orthogonal to everything else on this page. Key benefits:- Chosen per request. The same person, with the same key, can bill one call to an evaluation and the next to production support.
- One shared pot, with per-member slices. A project holds a single budget that everyone draws from, and can cap each member’s share of it at the same time.
- Can widen access, not only narrow it. A project can add providers, models and MCP tools on top of what the caller already holds, for the duration of that request only.
- Can pay instead of the caller. Spend can be taken off the caller’s key, profile, team and customer limits entirely.
- A first-class reporting dimension. Every log row, span, metric and warehouse export carries the project, so “what did this initiative cost” is a query rather than an inference.
To create your first project, skip to Configuration. The sections in between explain the fields that walkthrough asks you to fill in.
When to use a project
Every other governance entity describes a person or a credential. A project describes work. That gives a single decision rule:Ask whether the access should apply to every request this person makes, for as long as they hold it. If yes, it is an access profile. If it should apply only while they are doing a particular piece of work, and that work has its own budget, it is a project.So “contractors get
gpt-4o-mini only, with $50 a month” is an access profile and should stay one. “This evaluation has $5,000 and these six people can draw on it” is a project.
A worked example
An organisation has aplatform team and a data-science team, both under an acme-internal customer. Engineers sign in with SSO and hold an “Engineer” access profile: OpenAI gpt-4o-mini only, $200 per month. That is the right policy for their day-to-day work and none of it should change.
Leadership now wants to evaluate moving to Claude. Six named people, four from platform, two from data-science, plus one contractor, get $5,000 for the quarter to run comparisons on Anthropic models. The spend must not touch their personal budgets, must report as one line item, must stop dead at $5,000, and nobody else gets Anthropic.
As a project that is one entity: access rule Extend with an Anthropic provider config, accounting Project only, a $5,000 quarterly budget, split policy Equal, and six explicit members. The engineers keep their own identity and their own profile for everything else, and add x-bf-project-id to eval calls only. Those calls, and only those calls, reach Anthropic; the spend lands on the project’s $5,000 and is taken off their personal, team and customer limits; every log row carries project=claude-eval; and the equal split caps each member near $833 so one person cannot drain the pot.
Why not an access profile?
An access profile is the closest alternative, and it gets part of the way. It handles “these six users only” perfectly well, and if the eval profile allows Anthropic while the Engineer profile allows only OpenAI, the spend does land on the right ledger. Five things still break:- Assignments create one ledger per person, not a shared pot. A profile is a template; assigning it clones its budgets for each user. A $5,000 profile given to six people is six $5,000 budgets, so the initiative’s real ceiling is $30,000. Hand-setting $833 each recreates the cap but not the pot: it applies only at the top tier, and every roster change means rewriting every ledger by hand.
- The payer becomes unpredictable as soon as the two profiles overlap. A caller holding several profiles has one of them picked to pay, at random among those that permit the requested provider and model. An evaluation benchmarks Claude against the incumbent, so the moment both profiles permit
gpt-4othe comparison calls charge the engineer’s personal budget on roughly half of requests, with nothing in the request to say which. - It is always on. Access is the union of everything the caller holds, so they reach Anthropic on every request from assignment until detachment, including work unrelated to the eval. There is no per-request opt-in.
- Nothing marks a request as eval work. Access profiles are not a reporting dimension. “What did the eval cost” becomes “spend where provider is Anthropic”, which is wrong the moment the eval touches GPT for its baseline, or anyone uses Anthropic for anything else.
- The spend still rolls up the organisation. When a profile pays, the teams and business units above the user pay too, so eval spend consumes the platform team’s and the customer’s budgets. Only a project’s accounting mode can take a request off the caller’s tree.
Why not a team or a customer?
Teams and customers model reporting lines. A user’s team drives which data they can see and which budgets their ordinary spend rolls up through, so inventing aclaude-eval team to hold an initiative distorts both. Initiatives are also concurrent and short-lived where org position is singular and durable: a person belongs to one team for years while participating in several projects at once, each ending on its own schedule. And a team cannot be chosen per request, so it can never separate two kinds of work done by the same person.
Why not a shared virtual key?
A shared key gets the money right, since it is one pot with one cap, and loses the person. Every log row records the same key, so there is no per-person attribution and no way to cap an individual’s slice. Membership is unenforced: anyone holding the secret has the access. Most importantly a key replaces the caller’s identity rather than composing with it, so audit, RBAC and data access control all lose track of who actually ran the request.How a request resolves
Activating a project on a request
A request names a project with one of two headers:
The Use project button on a project’s page shows both, with a copy-ready request that sends one of them.
Send one or the other. If
x-bf-project-id is present at all it decides the outcome and the name header is ignored, including when its value is empty. An empty or unrecognised id resolves to no project, which is refused rather than quietly falling back to the caller’s own access.403 with error.type of access_blocked and the message project "<name>" not found. It does not exist or does not admit this request. That single answer deliberately covers both “no such project” and “you are not one of its members”: a caller who may not use a project has no business learning whether it exists.
A request that names no project is served exactly as it is today, against the caller’s own access and ledgers.
Resolution happens once, before the request is served, and every consumer downstream reads the resolved project rather than the caller’s header. The same funnel governs the LLM, streaming, realtime and MCP paths, so the behaviour is identical on all of them.
Membership
membership_mode decides who may use the project:
explicit(default) admits only the users on its roster.openadmits every authenticated caller. There are no member rows, so an open project cannot divide its budgets between members.
explicit project with an empty roster is usable by nobody. This is a legitimate intermediate state while you set one up, and the dashboard says so rather than treating it as an error.
Access rules
access_rule decides how the project’s own access composes with what the caller already holds. It is required, it applies uniformly to providers, models, key ids and MCP tools, and there is no per-resource variant.
intersect(Restrict) permits only what the caller and the project both allow.union(Extend) permits everything the caller already had plus what the project adds.
union, a project with no provider or MCP configs leaves access untouched. That is the cost-center shape: the ledger and the attribution apply, the access rule changes nothing.
Two details worth knowing. Provider weights do not compose, because two preferences have no meaningful intersection; where the project states a weight it wins as the more specific context, and otherwise the caller’s stands. And an unrecognised access rule permits nothing, since union is the mode that can widen a request and an unknown value must never be read as that.
Accounting modes
accounting_mode decides whose ledgers a request draws from. The deployment’s own global caps always apply: no accounting mode buys a request past them.
both enforces everything. A request has to fit inside the project’s caps and the caller’s own.
project_only takes the request off everything the caller funds, including their per-model budgets and the teams and business units above them. Use it when an initiative’s spend genuinely should not count against the people doing the work, as in the evaluation example.
principal_only is pure attribution: the project keeps its caps on record but checks and charges none of them, so the request is capped only by the caller and by the deployment. The project’s own Overview tab says so explicitly while this mode is set, because a funded project that charges nothing is otherwise a confusing thing to look at.
A scoped request whose caller holds no permit at all is refused under both and principal_only: those modes say the caller’s money moves, and there is nothing to take it from.
What a project allows
Provider and model access
A project holds at most one configuration per provider. Each one carries:
A list may not mix a wildcard with named entries:
["*", "gpt-4o"] is refused rather than interpreted. Key ids are validated when you save, and a key that does not exist or belongs to a different provider is rejected. A provider may carry up to 100 per-model budgets, each naming a concrete model; the * tier belongs on the provider configuration itself.
MCP servers and virtual MCPs
A project opens up MCP access two ways, and both only ever add: where they overlap, they union rather than cap. Per-client tool allowlists name an MCP server and the tools a project may execute on it.["*"] allows every tool the server exposes, including ones added later; an empty list allows nothing and, importantly, still counts as the project having named that server, so a server the project deliberately closed off cannot be reopened by a default-allowed rule.
Virtual MCPs are assigned by reference. Assigning one also makes it addressable at its slug endpoint for that request, exactly as a direct assignment to a virtual key would.
Tool allowlists union within the project, and then compose with the caller’s own MCP access through the project’s access rule: intersect caps what the caller may execute while inside the project, union extends it.
Budgets, limits and splits
A project’s budgets and rate limits are ordinary governance rows. They share every mechanic described in Budgets and Limits: reset durations, quarterly and fiscal windows, cluster-wide accounting and refusal behaviour. This section covers only what is specific to projects.Budget tiers
A project can hold money at three tiers, and a request is checked against all of the ones that apply to it:- The project’s own budgets and rate limit. Provider-agnostic: the same cap whichever provider serves the request.
- Per-provider budgets and rate limits, on a provider configuration.
- Per-model budgets and rate limits, on a model under a provider configuration.
402 with error.type of budget_exceeded; a rate-limit refusal is 429 as rate_limited, token_limited or request_limited. Rate limits are checked before budgets, so a caller who is both throttled and out of money gets the cheaper answer consistently.
Splitting caps between members
split_policy decides whether the project’s caps are shared as one pot or sliced per member.
none(default) holds everyone together in the project’s caps. Individual members can still be given their own caps by hand, from the Members tab.equalgives every member an equal slice of every budget and rate limit the project holds, at every tier, as a cap within it. The project’s own money, each provider’s, and each model’s all divide by head count.
equal that work is queued and runs in the background, with progress reported in the dashboard; under none only the affected member’s own caps change, and that happens immediately.
Redivision moves caps, never spend. Adding a seventh member to a $5,000 project drops everyone’s slice from about $833 to about $714, so a member who has already spent $900 is refused until the window resets. Removing a member widens everyone else’s slice the same way.
split_policy: none. Under equal the split owns those rows, so the dashboard hides the editor and the API refuses the request. Switching a project to equal replaces any hand-written member caps with the equal division, and the dashboard asks before doing it.
Calendar alignment
calendar_aligned moves a project’s windows onto the calendar rather than running them from the moment each budget was created. It applies uniformly to the project’s own money, every provider’s and model’s, and every member’s derived share, so a member who joins mid-month still resets with everyone else.
Windows shorter than a day cannot be aligned and stay rolling even on an aligned project. The dashboard hides the toggle when no window on the project is a day or longer.
Turning alignment on re-anchors existing windows and keeps the spend already recorded against them. Turning it off returns those budgets to windows measured from their creation, which can land the next boundary in the past and clear the current window’s spend on the next sweep. Prefer creating a project with the alignment you want.
Configuration
- Web UI
- config.json
- API
Navigate to Governance -> Projects in the Bifrost dashboard.The list shows every project you can see, with its providers, the budget closest to its cap, its access rule, its accounting mode, its member count, expiry and whether it is enabled. The Budget column shows the tightest cap across every tier, so a project whose per-model budget is nearly spent reads as nearly spent even when its overall budget is untouched.



Save with Create Project, or Create & Add Members when membership is explicit, which lands you on the new project’s Members tab ready to add its roster.
Budgets & limits is the full ladder: the project’s own caps, then each provider, with models nested beneath. Every row shows what has been used, what is left and when it resets. Deployment-wide caps appear when they are close to refusing, tagged Global. Rows that are not charged under the current accounting mode are dimmed with a footnote saying so.
Providers and MCPs are read-only views of what the project allows: allowed and blocked models per provider with weights, and tools per server plus assigned virtual MCPs with their slug endpoints.

Add Members takes users one at a time, or snapshots a team’s current roster as a convenience. Under an equal split the sheet previews what every member’s share becomes before and after, at every tier, so a roster change is never a surprise.
Clicking a member opens their usage, grouped project then provider then model. From there, Edit Caps gives that member their own cap on any budget or rate limit the project holds, as an amount or a percentage, with a live preview translating between the two. This is available only under 
Removing a member takes their caps with them and, under an equal split, redivides the rest.

Creating a project
Click Create Project.- Name and description. The name is globally unique and is what
x-bf-project-namerefers to, so pick something callers can type. - Enabled. On by default. A disabled project refuses every request that names it.
- Expires. Optional, with 7 days, 30 days, 90 days and 1 year presets or an explicit date. An expired project refuses every request that names it.
- Access. Who may use the project:
Explicitto admit only a roster you manage,Opento admit every authenticated caller.

- Access rule. Choose Restrict or Extend. The card explains the consequence of each, including that a Restrict project with no providers permits nothing.
- Providers. Add a provider, then choose all models or an explicit allowlist, optionally a denylist, which keys may serve it, a weight, and any per-provider or per-model money.

- MCP configurations. Choose which tools the project may execute under MCP servers, and assign any Virtual MCPs it should reach.

- Project budget and Rate limit. Add one or more budget lines, each with an amount and a reset window, then optionally a Maximum tokens and Maximum requests limit. Align to calendar cycle moves every window on the project onto the calendar.
- Accounting. Choose Both budgets, Project only or User only, and a split of No split or Equal.

The project detail view
A project’s page has five tabs, in this order: Overview, Budgets & limits, Members, Providers and MCPs. Each of the last three carries a count, and the Use project button beside them shows the headers a caller sends to reach the project.Overview opens with a Finish Setting Up checklist while anything essential is missing, then tiles for spend, tokens, requests, members, providers, MCP servers, the access rule, the accounting mode and when the project was created. A Needs attention card lists caps near or past their limit, and flags members left holding no slice of a cap under an equal split.




Managing members
Members lists the roster with each member’s tightest cap for spend, tokens and requests. Open membership shows an explanation instead of a roster, since there is none.

No split.

Enabling, expiring and deleting
The Enabled switch is on the list row and in the edit sheet; disabling asks for confirmation, since every request naming the project will be refused. Expiry is a field on the edit sheet.Deleting a project removes its providers, budgets and the spend recorded against them, and removes its members’ access through it. Log rows keep the attribution they already carry.
Attribution and reporting
Every request scoped to a project carries it through the whole observability stack, using the resolved project rather than the header the caller sent. Logs. Both request logs and MCP tool logs record the project’s id and name. The logs list filters on projects from the sidebar and has a Project column, hidden by default and enabled from the column picker. A log’s detail view links back to a filtered list for that project. The project filter section hides itself entirely until at least one project has appeared in your logs. Analytics.project is a ranking dimension and the dashboard has a Project Rankings tab, alongside cost, token and latency histograms by project. Requests carrying no project are reported under an Unassigned bucket rather than dropped, so the totals reconcile with real traffic and you can see how much of your spend is attributed.
Traces and metrics. Spans carry bifrost.project.id and bifrost.project.name. Prometheus and OpenTelemetry metrics carry project_id and project_name labels on both request and MCP metrics. Because a request is scoped to at most one project, these are always scalar; there is no plural form as there is for teams and customers.
Warehouse exports. BigQuery carries project_id and project_name columns, and the Datadog and Splunk exporters carry them as tags and attributes.
Visibility and permissions
Two independent controls apply, as they do everywhere in Bifrost Enterprise: RBAC decides what operations a role may perform, and data access control scopes the result set. TheProjects resource carries View, Create, Update and Delete. The dashboard hides what a role cannot do: without Update the enabled switch and edit action are disabled, and without Create the add-members action is unavailable.
Project visibility then follows the caller’s data access scope, and projects are unusual in two ways. Membership is a visibility path in its own right, so a user sees projects they created or belong to, where most resources are creator-only. And the roster inside a project is scoped as well as the project row, so two administrators can open the same project and see different member lists and different per-member caps.
See Data Access Control for how scopes are assigned.
Examples
An evaluation with its own funding
Six people from two teams get $5,000 for the quarter to evaluate a provider nobody personally has access to, without their own budgets being touched.A batch job confined to one provider
A CI principal with broad access should only reach OpenAI while running the nightly batch, with that spend reported separately.gpt-4o-mini on OpenAI and nothing else however broad its own access is. Both keeps the caller’s own caps in force as well. Open membership means the job needs no roster, which also lets it authenticate with a plain virtual key.
A cost center for one tool
An organisation wants everything done through a particular client capped and reported as one line item, without changing anyone’s access.x-bf-project-name: claude-code and its spend is capped at $5,000 a month and reportable on its own.
Next steps
- Access Profiles - the policy container for access that belongs to a person rather than to a piece of work
- Data Access Control - how project and roster visibility is scoped per role
- RBAC - assigning the
Projectspermissions - Budgets and Limits - reset windows, fiscal quarters and how cluster-wide accounting works
- Model Limits - per-model caps and the scope system
- Virtual Keys - the credential a project composes with
- config.json governance - the full declarative field reference

