Scoped sub-keys
A key with no scope will sign any 32 bytes you hand it. That is fine for a key a person drives through a confirmation dialog, and unacceptable for one an agent holds. A scope binds a key to a specific thing it is allowed to say.
What a scope binds
Scope bytes are [1-byte scheme][scheme-specific bytes]. Three schemes exist today.
| Scheme | Byte | Binds |
|---|---|---|
| Unscoped | 0x00 | Nothing — signs any hash |
| EVM UserOperation | 0x01 | entryPoint · chainId · sender |
| Solana transaction | 0x02 | wallet authority |
| EIP-712 | 0x03 | chainId · verifyingContract · typeHash |
The type hash matters as much as the contract
TransferWithAuthorization also sign permit on the same contract — which is an unlimited approval wearing a payment’s clothes. The scope commits to the primary type’s full field layout, so the two are different keys.Deriving the key ID
A sub-key’s suffix comes from its scope, not from a name you choose:
key_suffix = hex(sha256(scope)[:8]) key_id = oauth:<iss>:<sub>:<scope_hash>
The same user with the same scope always resolves to the same key, so minting is idempotent and there is no way to accumulate duplicate keys for one purpose.
Creating one
import { buildEIP712Scope } from "@oleary-labs/signet-sdk/scopedSign";
import { keygen } from "@oleary-labs/signet-sdk/keygen";
// USDC on Base, EIP-3009 transfers only.
const scope = buildEIP712Scope({
chainId: 8453,
verifyingContract: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
primaryType: "TransferWithAuthorization",
types: EIP3009_TYPES,
});
const key = await keygen(
{ groupId: GROUP_ADDRESS, nodeUrls: NODE_URLS },
keypair,
claims,
undefined,
undefined,
"ecdsa_secp256k1",
scope,
);Signing with one
A scoped key refuses a raw hash. You send the structured payload; every node parses it, re-derives the domain and type hash, checks them against the stored scope, and computes the signing hash itself.
import { signTypedData } from "@oleary-labs/signet-sdk/scopedSign";
const signature = await signTypedData({
nodeUrl: NODE_URLS[0],
groupId: GROUP_ADDRESS,
keySuffix: key.keyId,
curve: "ecdsa_secp256k1",
typedData,
sessionKeypair: keypair,
claims,
});That independent re-derivation is the whole guarantee. A malicious coordinating node cannot substitute a different payload, because the other operators never trusted its hash in the first place.
Where scopes stop
A scope constrains what may be signed — never how much or how often. A key scoped to USDC transfers on Base can transfer the whole balance, as many times as it likes. Value and rate limits belong in the smart account, where the chain enforces them against settled state at execution time.