# Getting a credential for citature

Written for an agent. Every step is a request you can make; nothing here needs a
person except the one step that does, and that step is marked.

Two paths reach a priced answer. An account, which every priced call over MCP is
billed to, needs a person: a token is issued only once somebody signs in and
approves your client, and no request of yours can do that for them. Paying per
call needs no person, and on Tempo's test network it is the sandbox:
[a sandbox, without a person](#a-sandbox-without-a-person).

## First: check whether you need one

Most of citature does not require a credential. The lookups — the tools that
restate a public federal record — answer without one, and they are the reason to
register the server at all: you can call one, read a full cited receipt, and
decide whether the paid half is worth anything to you, before any credential
exists.

Call `tools/list` against the endpoint. Every tool states its price in its own
description, and carries the same figures as fields in its `_meta` under
`com.citature/pricing`: the cents a call costs and the unit it is sold by, and
null for a free tool. The handshake's `instructions` name the priced
tools and their prices together. If the tool you want is free, stop here.

    POST https://mcp.citature.com/mcp

A priced tool can be called without a credential too, where citature has opened
per-call payment for it: each tool is also an HTTP operation on the same host,
and the operations open to per-call payment, with the offer each is paid by, are
listed in one document, served without a credential:

    GET https://mcp.citature.com/openapi.json

An open operation answers a call carrying no payment with 402 and a
`WWW-Authenticate: Payment` challenge, which a payment client pays with the
request; no account and no token are involved. An operation listed without an
`x-payment-info` offer is not open to per-call payment, and neither is a call
over MCP: for those, read on.

A token you present is validated whether or not the tool required one. An expired
or wrong-audience credential is refused rather than quietly served the free tier,
so a failing token is a failing token and not a silent downgrade.

## A sandbox, without a person

Per-call payment on Tempo's test network is paid in a test stablecoin the
network's faucet hands out, so no money moves, and it needs no account, no token
and no person. The answers are citature's own; only the payment is a test one.

An offer is on the test network when its `currency` is
`0x20c0000000000000000000000000000000000000`, the test network's stablecoin, and
the 402 challenge for it names chain `42431`, Tempo's Moderato test network.
citature's offers on the live network are in USDC,
`0x20C000000000000000000000b9537d11c60E8b50`, on chain `4217`, and cost real
money.

Where `/openapi.json` lists test-network offers:

1. Make a wallet and fund it from the faucet: the test network's public node,
   `https://rpc.moderato.tempo.xyz`, answers the JSON-RPC method
   `tempo_fundAddress`, whose one parameter is the address to fund.
2. Call an operation the document lists with an offer, carrying no payment, and
   pay the challenge its 402 carries with an MPP client, in the pull mode the
   challenge names. The payment library's own validator does both steps for
   every operation the document offers, with a wallet it makes and funds
   itself, paying each operation's documented example:

       npx mppx validate https://mcp.citature.com

   An example citature answers empty sends no payment and carries no
   `Payment-Receipt`, because an empty answer is served free, and the validator
   reports that check failed for its operation.

Where the document lists no offer, or only live-network ones, there is no
sandbox to reach, and the account path, below, is the other way to a priced
answer.

## Discover, without credentials

The resource server publishes RFC 9728 protected-resource metadata, served
without authentication:

    GET https://mcp.citature.com/.well-known/oauth-protected-resource/mcp

It names this resource's canonical URI, the authorization server for it and the
page that documents it (`resource_documentation`). A 401 from the endpoint
carries the same pointer in `WWW-Authenticate`:

    WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://mcp.citature.com/.well-known/oauth-protected-resource/mcp"

From the authorization server named there, read RFC 8414 metadata for the
endpoints and the algorithms:

    GET https://auth.citature.com/.well-known/oauth-authorization-server

Take every endpoint below from that document; the paths shown here are the ones it
names. Its `issuer` is the value your token's `iss` will carry.

The server also publishes a server card, in the MCP Server Card extension's v1
format, beside the endpoint and at the well-known path:

    GET https://mcp.citature.com/mcp/server-card
    GET https://mcp.citature.com/.well-known/mcp/server-card.json

It states what `initialize` answers with — the server's name, version and
description — with the endpoint, the protocol versions it accepts and, under
`_meta["com.citature/protected-resource-metadata"]`, the metadata document above.
It lists no tools: `tools/list` does, without a credential. Both paths are open
to a page on any origin: each answers a CORS preflight, exposes `ETag`, and
answers `If-None-Match` carrying the current tag with 304.

## Register a client

Registration is self-serve and needs no account (RFC 7591). Send your client's
metadata to the document's `registration_endpoint`:

    POST https://auth.citature.com/api/auth/oauth2/register
    Content-Type: application/json

    {
      "client_name": "<your client>",
      "redirect_uris": ["http://127.0.0.1:<port>/callback"],
      "token_endpoint_auth_method": "none"
    }

A `201` returns your `client_id` and the metadata as registered. What a
registration is held to:

- **Public clients only.** Every client is registered with
  `token_endpoint_auth_method` `none`: a request for a secret, or for no method,
  is registered as `none` and no secret is issued, so read the method from the
  response rather than from what you sent. PKCE stands in for a secret, and every
  authorization request must carry it. A registration carrying a key (`jwks` or
  `jwks_uri`) is refused `400` `invalid_client_metadata`.
- **No back-channel logout.** This server delivers none, and a registration
  asking for it (`backchannel_logout_uri` or `backchannel_logout_session_required`)
  is refused `400` `invalid_client_metadata`.
- **Redirects.** A loopback callback is `http` on `localhost`, `127.0.0.1` or
  `[::1]`, and its port may differ between registration and use. A client that
  lists no `https` redirect is registered as `native`, which is what permits a
  loopback callback; one that declares `application_type` `web` is held to
  `https`. Every other part of a redirect must match a registered one exactly.
- **Rate limited.** A `429` carries `X-Retry-After`, in seconds.

## Get a token

Authorization code with PKCE (RFC 7636, `S256`). Three things about this server
in particular:

- **Ask for the resource.** Pass `resource=https://mcp.citature.com/mcp` (RFC
  8707) at the authorization endpoint and again at the token endpoint. The token
  you get back names it in `aud`, and the resource server requires it to be one of
  the `aud` entries, compared exactly. With `scope=openid`, `aud` is an array that
  also names `https://auth.citature.com/api/auth/oauth2/userinfo`, so check for the
  resource among the entries rather than for equality. A flow that sends no
  `resource` is issued an opaque token instead, which the resource server cannot
  verify and refuses. A token minted for a different resource is refused even when
  it is otherwise perfectly valid — that check is what stops one compromised
  relying party opening every resource this issuer serves.
- **The scopes are the ones the metadata lists.** Present them at the
  authorization endpoint; a token missing a required scope is refused `403` with
  `insufficient_scope` naming what was missing.
- **Check `iss` on the way back.** The redirect to your callback carries `code`,
  your `state`, and `iss` (RFC 9207), which equals the metadata's `issuer`.
- **A request refused before sign-in.** A missing PKCE challenge or a scope the
  metadata does not list is refused before anyone signs in. The refusal is sent to
  a loopback or private-use redirect as `error`; for an `https` redirect it is
  shown on this server's own page, and your callback receives nothing.

**This is the step that needs a person.** The authorization URL, opened in a
browser, sends a person to sign in — or, when citature is taking new accounts,
to create one and confirm its address — and then to a consent screen naming your
`client_id` and where your code is sent: the host of an `https` redirect or, for a loopback or private-use
redirect, the whole URI, shown as an app on this device. Nothing is issued until
they approve. Tell whoever operates you what that will be, so the screen names
something they expect.

<!-- prettier-ignore -->
    GET  https://auth.citature.com/api/auth/oauth2/authorize
         ?response_type=code
         &client_id=<your client id>
         &redirect_uri=<your registered redirect>
         &resource=https://mcp.citature.com/mcp
         &scope=<from the metadata>
         &state=<yours>
         &code_challenge=<S256 of your verifier>
         &code_challenge_method=S256

A public client redeems the code with its verifier and its id:

    POST https://auth.citature.com/api/auth/oauth2/token
         grant_type=authorization_code
         code=<from the redirect>
         code_verifier=<your verifier>
         redirect_uri=<the same one>
         client_id=<your client id>
         resource=https://mcp.citature.com/mcp

Signatures verify against the issuer's key set, the metadata's `jwks_uri`:

    GET https://auth.citature.com/api/auth/jwks

## Use it

    Authorization: Bearer <token>

Name a metered call you may retry with an idempotency key, and send the same key
when you retry it. Billing dedupes on an identifier derived from your identity,
the tool, that key and the arguments — so a retry after a dropped connection is
billed once. The key goes in either of two places:

- the call's `_meta["com.citature/idempotency-key"]` — `callTool({ name,
  arguments, _meta })` in an MCP SDK client, which sets headers once for a whole
  transport and so cannot name one call with a header;
- an `Idempotency-Key` header.

Where a call carries both, the `_meta` key is used. A `_meta` key must be a
non-empty string; anything else is refused `bad_request` and costs nothing.
Without a key the JSON-RPC message id is used, which separates two deliberate
calls and dedupes only a retry that resends the same message under the same id,
on the same UTC day — an MCP SDK client gives its retry a new id.

## What the errors mean

Refusals arrive in two places, and you branch on them differently.

**Before a tool runs**, the resource server answers the HTTP request itself: an
OAuth error in the body, and a `WWW-Authenticate` challenge on the response.

| what you get | what it means | what to do |
| --- | --- | --- |
| `401` `invalid_token` | expired, wrong issuer, wrong audience, or bad signature | get a new token; check `resource` matched |
| `401` `unauthorized` | no `Authorization` header on a priced tool, or on a tool name this server does not publish | read the metadata the challenge points at |
| `403` `insufficient_scope` | the token is valid and lacks a scope | re-authorize with the scope named in the response |
| `429` `rate_limited` | this caller has spent its requests for the window | wait the seconds in `Retry-After`; the message states the same figure |

Requests are counted per caller, over a fixed window: against the `sub` of a
token that checks out, and otherwise against the address the gateway observed —
so a credential gets its own bound rather than sharing one with everybody
arriving from the same place. A call whose token does not check out is counted
against its address like an uncredentialed one, and past the bound it is answered
`429` rather than `401`; both refusals are free, and only one of them tells you
how long to wait. A refusal names the bound, the window and the seconds to wait,
and costs nothing: the count is taken before anything is answered, so a refused
call reads no data and reaches no meter. Sending the same credential from more
machines does not raise the bound; it divides it.

Every response the bound counted states it, in the `RateLimit-Policy` and
`RateLimit` fields of the IETF draft `draft-ietf-httpapi-ratelimit-headers`, so
you can pace yourself before a refusal rather than learn the bound from one:

    RateLimit-Policy: "subject";q=<requests>;w=<seconds>
    RateLimit: "subject";r=<requests left>;t=<seconds until the window rolls>

The policy is `subject` for a call counted against a token's `sub` and `address`
for one counted against the address it arrived from. A `429` carries `r=0`, and
its `t` is the figure in `Retry-After`. The discovery documents, `/robots.txt` and
the host's root, and a request refused before a credential is looked at — an
unknown path, a wrong method, a body that is not a JSON-RPC request or
notification, which is every `-32600` and `-32700` — are not counted and carry
neither field.

**Once a tool is dispatched**, the refusal is a tool result: `isError` is set and
`_meta["com.citature/error"]` carries an `error` code, the `message` and a
`status`, so you branch on the code rather than parse the sentence. A refusal
carries no `structuredContent`: that field is held to the tool's output schema,
which describes an answer. Arguments that fail a tool's
input schema refuse this way too, and that message names the tool and every rule
that failed. An argument the schema does not name is one of those rules: it is
refused rather than ignored, so a misspelled filter never runs as a question
without it, and the schema says so as `additionalProperties: false`.

| `error` | what it means | what to do |
| --- | --- | --- |
| `bad_request` | the arguments do not satisfy the tool's schema, an argument it does not name included, or the answer renders past the size one response carries | the message names the tool and the field; fix and retry |
| `not_found` | no tool by that name | read `tools/list` for the names this server serves |
| `upstream_unavailable` | a source citature reads is not answering | retry with backoff |
| `internal` | a fault on our side | retry with backoff; nothing was charged |

A question that matched no record is not a refusal: it is an ordinary answer with
an empty `answer` and a full receipt. It is never billed, and neither is a
withheld estimate nor any refusal above.

## How long a credential lasts

The resource server checks a token by its signature and never asks the
authorization server about it, so a token stays valid until the `exp` it carries —
the token response's `expires_in` gives the same span. To stop using a credential,
discard it.

A client registered this way has no secret and belongs to no account, so there is
nothing to rotate or hand back. Approval is recorded per client and per account: a
later authorization request within what was approved returns a code without the
consent screen, as long as the person is still signed in.
