A customer asks an AI agent to pay their bills. That agent hands parts of the job to other agents, and they call APIs. Every hand-off is a chance for the task to drift from what the customer asked for. Here, every entity in the chain is checked against the purpose of the agent the customer started with, the origin, before it gets a token and again when it uses one.
Three things make that possible. Every agent and API is an OpenID Federation entity whose record says what it does in BIAN terms, the banking industry's service vocabulary. A small language model reads those records and reports what their words actually describe. PingAuthorize makes every decision, using the bank's own rules.
1The architecture
Agents act. The authorization server and every API ask PingAuthorize before anything happens. PingAuthorize asks the purpose service for evidence, and only uses the model when the evidence needs reading.
Scroll sideways to see the whole diagram.
2Where purpose is checked
Whether an entity's words fit its declared codes can be checked in two places, and the demo runs every scenario both ways: the switch is above its scenarios. At the PDP, the bank's purpose service runs the model on each record. In the federation's policy, each superior writes the check into its Subordinate Statement as a metadata policy, and the trust anchor's resolver runs the model to apply it: an entity whose words fail it does not resolve, for anyone. What stays put is who decides fit to the task: PingAuthorize, on the bank's purpose map, either way.
Scroll sideways to see the whole diagram.
purpose_statement must describes its declared codes, and lists describes in metadata_policy_crit. OpenID Federation lets a federation add operators like this (6.1.3.2); a critical one a relier cannot evaluate invalidates the chain (3.1), so a relier without a model resolves through the trust anchor's resolve endpoint (8.3), where the model is. The resolver applies the policy once for everyone and signs what resolved, or refuses (8.9, invalid_metadata). The resolver never sees the task, so fit to the task stays with the bank.| At the PDP | At the federation's resolver | |
|---|---|---|
| Who runs the model | Each bank, for its own decisions | The trust framework's resolver, once for every relier |
| Where the rule lives | PingAuthorize's policy: rule 5, "what an entity declares must match what it describes" | The superior's Subordinate Statement: purpose_statement describes the codes, critical |
| What happens to a misfit | Denied by the bank, with the model's quote | Does not resolve at all: rule 1, for every relier, with the resolver's reason |
| What the PDP trusts | Its own model | The resolver's signature on the resolve response, with the trust chain it was derived from |
| Fit to the task | The bank's purpose map; outside it, the PDP's model may deny another purpose | The same, at the PDP: the resolver never sees the task |
| Suits | A bank that wants its own model, prompts and audit trail | A federation whose superiors vouch for purpose, and want one check for everyone |
3One decision, step by step
PayRoute, which Bill Pay handed the payment to, asks for a token for the Payments API. This is what happens between the request and the token.
Scroll sideways to see the whole diagram.
sub, the Payments API as its only audience, and the chain PayRoute ← Bill Pay in act. When PayRoute then calls the Payments API, its PEP asks PingAuthorize again (steps 3 to 10): a member dropped since the token was issued is refused at that point.4Who is in a decision, and where the denies land
For one diagram per scenario, drawn from recorded runs, see Use cases. This section is the general picture.
Every decision has the same cast. The customer is the subject, and is not judged. The entities judged are the ones in the token's act chain, from the current actor down to the origin, plus the audience being asked for. The authorization server asks at a token exchange and a PEP asks at a call; PingAuthorize decides, with the purpose service, the federation's resolver and the model behind it. The ten decisions run in order, and the first that fails is the reason the caller sees.
Scroll sideways to see the whole diagram.
Scroll sideways to see the whole diagram.
| Reason the caller sees | Rule | Decided by | Lands at | Token | Model | Scenarios |
|---|---|---|---|---|---|---|
chain_invalid | 1 | The federation resolver: signatures, and the purpose policy | AS, or a PEP | None at the AS; at a PEP, one in flight is refused | Not asked | Revoked member; Revoked in the middle; under the policy also Codes pass, words do not; Passed on to others; An agent for everything |
secondary_use_declared | 4 | Facts, from the declared codes | AS | None issued | Not asked | Purpose drift; The bank's own marketing; Drift three hops down |
declared_vs_described | 5 | The judge, on the model's reading | AS | None issued | Read the record | Codes pass, words do not (at the PDP) |
inconsistent | 7 | A rule on the reading (use for other organisations), or the model outside the map | AS | None issued | Read; judged fit | Passed on to others (at the PDP); Wrong resource; Borrowing to pay |
held_for_customer_approval | 8 | The judge: unsure, not needed, too broad, or no answer in time | AS | None issued | Read; judged fit | A helpful extra; Fits the delegator, not the origin; An agent for everything (at the PDP) |
prescreen_flagged, origin_gate, secondary_use_described, no_rule_applied | 2, 3, 6, 10 | Facts, the judge, the policy's default | AS or PEP | — | — | Not shown by a scenario; the policy build tests each |
5What the token carries
The token PayRoute gets in step 11. The whole delegation chain travels with it, and PingAuthorize reads all of it.
{
"iss": "…/e/as",
"sub": "alice@northwind.example",
"aud": "…/e/payments-api",
"client_id": "…/e/payroute",
"scope": "bill_pay",
"act": {
"sub": "…/e/payroute", ← current actor
"act": {
"sub": "…/e/bill-pay" ← origin
}
},
"cnf": { "jkt": "…" } ← DPoP key
}
- Delegation, not impersonationAlice stays the subject. Each agent that took part is added to
act, the newest outermost. - The origin bounds the chainThe innermost actor is the agent Alice started with. Its purpose, as its federation record declares it, is what every later entity is checked against.
- Read on purposeThe token exchange standard, RFC 8693, treats earlier actors as informational. For agents, where a request came from is the point, so this design reads the whole chain, and only PingAuthorize's rules read it.
- Every name resolvesEach
act.subis a federation entity identifier, so its record, and who vouches for it, can be fetched and checked. - Onward only when approvedWhen the audience is itself an agent, the token also carries
may_actnaming it. Only that agent can exchange it onward, and only because PingAuthorize permitted it as the audience.
6What an entity statement carries
Every agent and API states its mission and purpose in clear text in its federation metadata, and its superior vouches for them. This is the registry's Subordinate Statement about PayRoute with purpose checked in the federation's policy, as the demo serves it, with its keys left out. The demo's "Entity statements" view shows every entity's, with the resolver's response beside it.
{
"iss": "…/e/registry", ← its superior signs
"sub": "…/e/payroute",
"metadata": {
"oauth_client": {
"display_name": "PayRoute Orchestrator",
"mission": "Get each bill payment to the
biller by the fastest safe route.",
"purpose_statement": "Routes customers' bill
payments to the fastest available payment
rail and confirms settlement.", ← what the SLM resolves
"description": "Fast, reliable routing
for bill payments."
}
},
"metadata_policy": {
"oauth_client": {
"authorization_details_types": {
"subset_of": [
"…/initiate_payment_order_initiation",
"…/execute_payment_rail" ] ← codes capped
},
"purpose_statement": {
"describes": "authorization_details_types"
} ← the purpose policy
}
},
"metadata_policy_crit": ["describes"] ← must be understood
}
- Mission and purpose, in clear textThe mission says why the entity exists; the purpose statement says what it does and for whom, in words a person and a small model can both read. The description is for people.
- Vouched, not self-assertedThe registry sets these in its statement's
metadata, which replaces whatever PayRoute wrote about itself (OpenID Federation 6.1.4.2). An entity whose superior does not vouch for its words is marked self-asserted. - Codes cappedThe registry's
metadata_policylimits the BIAN capabilities PayRoute may declare, whatever it claims. - Purpose as policyThe
describesoperator says the purpose statement must describe the declared codes and nothing else. Listed inmetadata_policy_crit, it cannot be skipped: a relier either evaluates it or does not resolve the entity. The trust anchor's resolver evaluates it with the model, so reliers resolve through the resolver. - What the SLM makes of itPayment Rail, Execute, serving the customer: the policy holds, and PayRoute resolves. The model reads all three fields, so words that give an entity away count wherever they are.
- This demo's convention
mission,purpose_statementand thedescribesoperator are not defined by OpenID Federation. The standard's informational metadata hasdescription(5.2.2), and it allows federations to add policy operators (6.1.3.2); the rest is proposed here.
7Deeper chains
A chain is as long as the task needs. Each token exchange puts the agent that made it outermost in act, and the origin stays innermost. At every hop the whole chain is judged against the origin, not against the agent before it.
Scroll sideways to see the whole diagram.
Revoked in the middle
The registry drops PayRoute while Payee Check holds a valid token. A check of the current actor alone, which is what RFC 8693 asks of a token's consumer, sees only Payee Check, still a member in good standing, and lets the call through. Resolving every agent in the chain finds PayRoute gone, and the call is refused without asking the model.
Fits the delegator, not the origin
PayRoute asks for a token for DebitLink, which registers direct debit mandates. Judged against PayRoute, a payment router, that is within purpose, and PingAuthorize says so when the demo asks it that way. Judged against Bill Pay, it is outside the purpose map: the customer asked for bills to be paid, not for billers to hold standing authority over the account. So it is held for the customer to approve. Only the origin decides.
8What drift looks like
Drift is anything in the chain doing something other than the task the customer delegated. Every entity below resolves in the federation and is in good standing: what is wrong is the purpose, not the membership. Each is a scenario in the demo.
| Kind | Example | How it is caught |
|---|---|---|
| A secondary use, declared | Spend Insights scores customers for partner offers (Purpose drift). Northwind's own Offers API matches customers to products (The bank's own marketing). | Rule 4, from the declared codes. No model. |
| Drift deep in the chain | Three agents from the origin, Payee Check asks for Biller Connect, which shares customers' details with billers' relationship teams (Drift three hops down). | Rule 4, still judged against Bill Pay. No model. |
| A secondary use in the words | QuickPay's codes say payments; its description says it sells payee data (Codes pass, words do not). | Rule 5 at the PDP: the model reads the words and quotes them. In the federation's policy, rule 1: QuickPay does not resolve. |
| Use for other organisations | Receipt Relay sends receipts, and passes payment history on to credit reporting agencies (Passed on to others). | Rule 7 at the PDP: the quoted words show data going out to other organisations. In the federation's policy, rule 1: it does not resolve. |
| A different task | BillBridge lends the shortfall (Borrowing to pay). The Credit Rating API (Wrong resource). | Rule 7: outside the purpose map, and the model finds it serves another purpose: a different product, or the bank's own credit decisions. |
| A helpful extra | BillNudge sends bill reminders (A helpful extra). | Rule 8: outside the map, and the model finds the task does not need it but that it serves no other purpose. Held for the customer, not refused. |
| Fits the delegator, not the origin | DebitLink suits PayRoute, a payment router, but not paying Alice's bills (Fits the delegator, not the origin). | Rule 8: held, because the check is against the origin, whose purpose map has no mandates. Against PayRoute alone it would be permitted. |
| Too broad to place | Money Mate helps with anything to do with money (An agent for everything). | Rule 8 at the PDP: it cannot be shown to serve the task, so it is held for the customer. In the federation's policy, words that broad describe nothing, and it does not resolve. |
Outside the purpose map the model may deny only what serves another purpose: marketing, profiling, data for other organisations, the bank's own planning, risk or credit decisions, or a different product. Everything else outside the map, including a helpful extra, is held for the customer. Nothing the model says can permit it.
9The rules, in order
PingAuthorize applies the first rule that matches, and its reason is what the caller sees. The first four rules need no model: when one of them decides, the model is never asked. The rule names are the policy's own.
| # | Rule | Decided by | Shown in |
|---|---|---|---|
| 1 | Every entity must resolve in the federation | Signatures, and the federation's policy | Revoked member; Revoked in the middle; in the federation's policy also Codes pass, words do not; Passed on to others; An agent for everything |
| 2 | No text may instruct the checker | Pre-screen | Passes in Injection neutralised: the registry's words replace BillSaver's first |
| 3 | The origin's purpose must be vouched for and specific | Superior's statement | Passes in every scenario: Northwind Bank vouches for Bill Pay |
| 4 | No entity may declare a secondary-use Service Domain | Declared codes | Purpose drift; The bank's own marketing; Drift three hops down |
| 5 | What an entity declares must match what it describes | The model's reading | Codes pass, words do not |
| 6 | No entity may describe a secondary-use Service Domain | The model's reading | — a backstop: rules 4 and 5 catch these first |
| 7 | Every entity must serve the origin's purpose | The model's reading | Wrong resource; Borrowing to pay; Passed on to others |
| 8 | Hold what cannot be placed | An extra, unsure, or no answer | An agent for everything; A helpful extra; Fits the delegator, not the origin |
| 9 | Permit when every entity serves the origin's purpose | The purpose map | Happy path; Injection neutralised; Three agents deep |
| 10 | Otherwise deny | Default | — |
Rules 4 and 6 use the bank's secondary-use list, such as Customer Behavior Insights. Rule 9 permits only when every entity's Service Domains, declared and described, are inside the purpose map for the origin's task. Rule 7 denies an entity whose words show use for other organisations, or one outside the map that the model finds serves another purpose. Everything else outside the map is held (rule 8): what the model thinks fits, a helpful extra the task does not need, anything too vague to place, and a model that does not answer in time. Rule 6 is a backstop: as the services stand, rules 4 and 5 catch a secondary use first.
10Who vouches for whom
The federation is where the words the model reads come from, and who stands behind them.
Scroll sideways to see the whole diagram.
11What the model does, and what it doesn't
The model
- Reads one record at a time, its mission, purpose statement and description, and names the BIAN Service Domain they describe. It runs at the PDP, or in the federation's resolver as the
describespolicy operator. - Points out activity the codes don't declare, and use for other organisations, quoting the words. A claim whose quote is not really in the record is dropped.
- Flags text that addresses the checker.
- Outside the purpose map, may deny what serves another purpose. It never permits: a helpful extra is held for the customer.
- Runs inside the private network, so no decision goes to an outside model provider.
Not the model
- Whether an entity is a member at all: federation signatures.
- What a purpose may draw on: the bank's BIAN purpose map, held in policy.
- What counts as secondary use: a policy list.
- The decision itself: PingAuthorize's rules.
- Keys and tokens: standard OAuth and federation checks.
Small models are reliable at placing a description in a fixed vocabulary, and unreliable at open-ended "does this serve that purpose?" judgements. So the model does the first job, and the bank's purpose map does most of the second, with the model allowed only to deny, and only for another purpose.
Choosing the model: what these jobs ask of a model, and how a typed decision model would do them.
12Standards used
- OpenID Federation 1.0: entity records, trust chains, metadata policy with an additional operator (6.1.3.2) and
metadata_policy_crit, trust marks, the resolve endpoint (8.3) - RFC 8693 OAuth 2.0 Token Exchange: delegation, the
actchain,may_act - RFC 9449 DPoP: every token bound to its holder's key
- RFC 7523:
private_key_jwtclient authentication - RFC 9396 Rich Authorization Requests:
authorization_details_typeson agents - RFC 9728 Protected Resource Metadata:
authorization_details_types_supportedon APIs - OpenID AuthZEN 1.0: every policy question
- BIAN Service Landscape v14.0.0: the vocabulary records are written in
13How it is deployed
One private network: the federation, the authorization server, PingAuthorize, the purpose service, the model, this site, and every agent and API of the story as a service of its own, with its own keys and its own PEP. Only this site and the federation face the internet. The agents and APIs, PingAuthorize, the purpose service and the model can be reached only from inside; the federation's resolver reaches the model the same way. What's deployed reads each service live: versions, the policy package, the model's digest, and whether each is answering.
Run the demo
