How Uno uses MPP
This page explains how Uno applies the Machine Payments Protocol (MPP) to Kit storage purchases. It is for developers who must implement or debug a payer. It defines the protocol terms, maps them to what Uno ships, and traces one full 402 request flow.
MPP is an HTTP authentication scheme named Payment. A server answers an unpaid request with HTTP 402. The payer pays and retries. The server answers with the resource and a receipt. Source: Payment HTTP Authentication Scheme, draft 01, section 4.1. Stripe and Tempo describe MPP as an open protocol for agents that pay for APIs without a checkout UI: Stripe MPP docs.
Terms
| Term | Meaning |
|---|---|
| Challenge | A WWW-Authenticate: Payment header. It states what the server wants paid. |
| Credential | An Authorization: Payment <credential> header. It carries the echoed challenge and the proof of payment. |
| Receipt | A Payment-Receipt header on a successful response. It names the method and a method-specific reference. |
| Method | The way value moves, such as tempo or stripe. It is a lowercase identifier. |
| Intent | The kind of payment request, such as charge. |
Source: draft 01, section 3 and sections 5.1 to 5.3.
A challenge has these required parameters: id, realm, method, intent and request. It may also carry expires, digest, description and opaque. The request value is base64url JSON whose fields the method defines. The server binds id to the other parameters, so a payer cannot change them. The payer echoes the challenge back inside the credential and must not modify opaque. Source: draft 01, section 5.1.
What Uno ships
Uno sells Kit storage in 100 MB blocks. The challenge is the same MPP object for every account type, but the offers differ.
| Account | Offers | Notes |
|---|---|---|
| App | tempo charge. stripe charge through Link, when offers are open and Stripe is configured. tempo subscription. | A purchase challenge expires 600 seconds after it is minted. A subscription starts with uno kit pay --subscribe. |
| Agent | tempo charge only. | The purchase has a frozen quote. Stripe is refused. App block purchases and subscriptions are refused. |
An Agent offer stays closed until its network opens its offers. Until then, the storage snapshot has no pricing, a quote shows no price, and pay_url is empty.
The 402 request flow on Kit
Kit is served at https://kit.uno.fun. An App owner reaches the flow through uno.kit.v1.AccountService/Pay, or through a storage write that the server refuses until payment. An Agent owner creates a purchase, then pays its link at https://mpp.uno.fun.
- The payer sends an unpaid request. For App, this is
Paywithblocksandrenew, or a write that exceeds quota. - The server answers HTTP 402 with
Cache-Control: no-storeand aWWW-Authenticate: Paymentheader. Connect code isresource_exhausted. - The payer reads the challenge and checks the amount, token, chain and recipient against what it intended to buy.
- The payer pays on Tempo, or approves a Stripe Shared Payment Token through Link.
- The payer repeats the request with
Authorization: Payment <credential>. The credential echoes the challenge unchanged. - The server verifies the challenge and the payment. It answers HTTP 200 with a
Payment-Receiptheader.
Source: draft 01, section 4.1.
A payment challenge response sets one WWW-Authenticate: Payment header for each offer. A write that exceeds quota carries only the first offer in the header. Its PaymentChallenge detail still lists every offer.
On success the server sets Payment-Receipt from the method and reference. A request that paid the purchase earlier returns the original receipt. Replaying the same credential does not charge twice.
The spec forbids a Payment-Receipt on an error response. A failed credential check gets a 402 with a fresh challenge. Source: draft 01, sections 4.2 and 5.3.1.
Challenge fields Uno sets
| Field | Value |
|---|---|
realm | kit.uno.fun |
method | tempo or stripe |
intent | charge, or subscription for a Tempo subscription |
request | Base64url JSON. See the Tempo request fields below. |
expires | RFC 3339 time. 600 seconds after minting for an App purchase. An Agent challenge expires with its frozen quote. |
opaque | Base64url JSON that binds the owner, the purchase and a random nonce. An Agent challenge also names the quote. |
A payer must treat opaque as a sealed value and echo it as it received it. Uno computes the challenge id as an HMAC over the other fields, expires and opaque included. A changed request, a longer expires or a wrong key fails with Connect code permission_denied and reason PAYMENT_CHALLENGE_INVALID.
Tempo request fields
The request of a tempo charge challenge holds these fields. Uno sets amount, currency, recipient, methodDetails.chainId, methodDetails.feePayer and methodDetails.supportedModes on every challenge. The two purchase flows described on this page also set externalId and methodDetails.memo.
| Field | Meaning |
|---|---|
amount | Integer string in token base units. The Agent offer formats the amount with 6 decimals, so 5000000 is 5.000000 tokens. |
currency | The TIP-20 token contract address. |
recipient | The payee address. |
externalId | The merchant reference. It is the purchase id. |
methodDetails.chainId | 42431 for Moderato (testnet) or 4217 for Tempo Mainnet. |
methodDetails.feePayer | false. The payer pays the network fee. |
methodDetails.supportedModes | ["push", "pull"]. |
methodDetails.memo | A bytes32 hex value. The transfer must carry it. |
Source: Tempo charge draft 00, section 4, Tempo network details.
Uno sets the memo to the keccak256 of the purchase id. A transfer also passes when it carries the MPP attribution memo for the challenge. The attribution memo is the mpp tag, version 1, part of the realm hash, 10 client bytes, and part of the challenge id hash. A plain transfer without a bound memo is refused.
The decoded request has this shape. The values below are placeholders, not a live quote.
{
"amount": "5000000",
"currency": "0x20c0000000000000000000000000000000000000",
"recipient": "0x00000000000000000000000000000000000000aa",
"methodDetails": {
"chainId": 42431,
"feePayer": false,
"supportedModes": ["push", "pull"],
"memo": "0x<32 bytes in hex>"
},
"externalId": "<purchase id>"
}Uno does not capture a full wire 402 in a test. Read a real one from the WWW-Authenticate header of your own request and decode request and opaque as base64url JSON.
Networks
| Network | Chain id | Use |
|---|---|---|
| Moderato | 42431 | Test. The challenge offer has test_mode set. |
| Tempo Mainnet | 4217 | Production. |
A Uno service accepts only these two chain ids. The offer names the chain. Pay only the chain and token that the challenge names.
Credentials: push and pull
A Tempo credential proves payment in one of two ways. The challenge advertises both.
| Mode | Credential payload | Who broadcasts |
|---|---|---|
| Push | type hash with the transaction hash (0x and 64 hex characters) | The payer broadcasts, then sends the hash. Uno reads the receipt from a Tempo node. |
| Pull | type transaction with the signed transaction | The payer signs. Uno broadcasts. |
Source: Tempo charge draft 00, sections 4.3, 5.2 and 5.3.
For a hash credential, Uno checks the node chain, then the transaction receipt. The receipt must show success and a transfer of the exact token, amount and recipient with an accepted memo. A receipt that is not on chain yet is a normal wait.
A credential has a size limit of 32,768 bytes.
If a reply is lost after a payment, retry the same credential. Do not pay again unless the payment is known to have failed.
Stripe
A stripe charge offer is for App accounts. It is in USD cents with methodDetails.paymentMethodTypes set to ["card"], plus networkId and livemode. The payer sends a Stripe Shared Payment Token as payload.spt. The receipt reference is the Stripe PaymentIntent id.
An Agent owner gets no Stripe offer. Kit does not start a Stripe payment attempt for an Agent purchase. It refuses the credential, with reason AGENT_BLOCK_PURCHASE_UNSUPPORTED when the writer rejects the claim.
Receipts
| Method | reference |
|---|---|
tempo | The transaction hash, 0x and 64 hex characters. |
stripe | The Stripe PaymentIntent id, which starts with pi_. |
A subscription receipt also carries the subscription id. Source: draft 01, section 5.3.
The Connect error detail
Over ConnectRPC, a 402 response is a Connect error with code resource_exhausted. It has one uno.kit.v1.PaymentChallenge detail. Read it with ConnectError.findDetails(PaymentChallenge).
| Field | Meaning |
|---|---|
challenge_id | The challenge id. |
pay_url | A link where a person pays. |
amount, currency | The primary offer, as a decimal amount and a currency name. |
mpp_challenge | The primary challenge. Pass it on unchanged and do not parse it for payment. |
expires_at | Unix seconds. |
offers | A list of PaymentOffer: method, intent, amount, currency, mpp_challenge, expires_at, test_mode. |
Reads never return this error for payment. An Agent write that exceeds capacity gets Connect code resource_exhausted with HTTP 403 and a LimitExceeded detail, not a PaymentChallenge. The LimitExceeded detail names a reason such as AGENT_CAPACITY_EXCEEDED.
What is not built
- Tempo sessions. The
TempoSessionServiceproto exists, but its service is not rebuilt. - More than one method for an Agent owner. An Agent offer is
tempochargeonly. - Payment inside MCP. Uno MCP tools return a
payment_requiredresult, not the MCP payment transport draft.
Next
Pay a challenge with Tempo: /mpp/pay-with-tempo. Pay with Stripe Link: /mpp/pay-with-stripe-link. Recover an uncertain payment: /mpp/recover-a-payment.