Learn / Lesson 4
Keys that cannot raise their own limit
Separate spending authority from limit management. This local model keeps a fixed total and does not let the spending key raise it. [2]
quotum x402 per-request intake remains closed. Do not sign a per-request payment. [2]
Who can move the ceiling?
A per-payment cap and a session budget answer different questions. The x402 v2 specification leaves client-side budget management outside its scope. If you are building a spending tool, decide both how much it may authorize and who may change that rule. Treat the management credential as a separate part of your design. [1]
For this lesson, imagine a fictional provider with a spending key and a separate operator. The spending key can use an allowance; it cannot edit it or create keys. This is a teaching model, not a description of a named provider API. We will test its permission boundary instead of assuming a cap enforces itself. [1]
Draw the authority boundary
Start by listing everything the spending process can reach. Include credentials in environment variables, configuration files, mounted secrets and tools. Look for a management key or a funded wallet that could obtain another credential. Remove those routes from the spending process before treating its visible cap as a meaningful boundary. [1]
In our fictional example, a spending key is labeled with a sample limit of 20 units. If the same process also holds an operator credential with access to a sample account of 100 units, assess the whole account balance as reachable. The smaller label alone does not describe the authority you handed over. [1]
Keep limit changes in a separate operator action. This toy refuses every raise. A spending role cannot manage limits. A holder role cannot either: limit changes would be a separate operator action, and this toy does not implement that action. It does not parse free text. [1]
What the quotum docs actually say
The seat docs describe one provider key per seat, limited to the session cap. The keeper sets that limit at the provider. When a seat is freed, its key is deleted. The docs do not identify the key type, so this lesson does not label it an admin or inference key. [2]
Holder day keys are off since 30 October 2026. The docs describe a separately budgeted day key for the house, allocated from the operator’s seat. That use is separate from holder issuance. This page cannot create one; its allocation control is a fictional exercise. [2]
Pay as you go is another system beside seats: revocable project keys spend prepaid balance through its endpoint. Its project balance and key budget are separate from the seat session cap. Keep those scopes separate when reading a meter or checking a refusal. Do not treat the example allocation here as a live product allowance. [2]
Share an allowance without growing it
The local example starts with a parent limit of 50 sample units. Only the holder role may allocate a child. Allocating 5 lowers the parent limit to 45 and gives the child 5. The combined limit remains 50. These numbers define the exercise; they are not a quote, a funded balance or a measured spend. [1]
The model checks available parent allowance before cutting a child. It refuses a non-integer, zero, a negative amount, an allocation above 5, an allocation above unspent parent allowance, an eleventh child, and any non-holder caller. Spending reduces what remains available without raising any limit. Reset starts a new local example. [1]
Try the refusal first
Ask the spending key to raise its own limit. Then ask it to allocate a child. Both actions should be refused in this toy. Choose the holder role to allocate 5 sample units and inspect both sides of the split. Repeat the test after spending, when less unspent parent allowance remains. [1]
For your own implementation, write tests for limit changes, credential creation and spending beyond the allowance. Check that failed actions leave the accounting unchanged. Use disposable test credentials in a sandbox and inspect the actual provider response. The plain REFUSED labels below are model outcomes, not promises about a provider HTTP status code. [1]
Combined limit: 50 sample units
Parent limit 50, spent 0, child limits 0.
Choose an action. Holder day keys on quotum are off.
Run it
Self-contained JavaScript sample. Run here or save the module and run it with Node. No network or credentials.
// Fictional provider, sample units, no credentials or network.
export function createFamily() {
let parent = 50, spent = 0;
const children = [];
const valid = n => Number.isSafeInteger(n) && n >= 0;
return {
raise(role, limit) {
if (role !== 'operator') return 'REFUSED: only an operator action may change limits';
return 'REFUSED: raising the original allowance is outside this toy';
},
cut(role, amount) {
if (role !== 'holder') return 'REFUSED: holder authority required';
if (!valid(amount) || amount === 0 || amount > 5 || children.length >= 10 || amount > parent - spent) return 'REFUSED: invalid allocation';
parent -= amount;
children.push({ limit: amount, spent: 0 });
return 'ALLOCATED: parent ' + parent + ', children ' + children.reduce((s, k) => s + k.limit, 0);
},
spend(amount) {
if (!valid(amount) || amount > parent - spent) return 'REFUSED: allowance exceeded';
spent += amount;
return 'SPENT: ' + spent + ', parent remaining ' + (parent - spent);
},
state() { return { parent, spent, children: children.map(k => ({ ...k })), total: parent + children.reduce((s, k) => s + k.limit, 0) }; }
};
}
export function run() {
const f = createFamily();
return [f.spend(10), f.raise('spending', 500), f.cut('spending', 5), f.cut('holder', 5), f.spend(35), f.spend(1), 'TOTAL LIMIT: ' + f.state().total];
}
if (typeof process !== 'undefined' && process.argv[1]?.endsWith('/keys-that-cannot-raise-their-own-limit.mjs')) console.log(run().join('\n'));
The sample has not run yet.
