Skip to content

Add createx402DelegationProvider to @metamask/smart-accounts-kit/experimental#248

Merged
jeffsmale90 merged 17 commits into
mainfrom
feat/delegation-provider
May 27, 2026
Merged

Add createx402DelegationProvider to @metamask/smart-accounts-kit/experimental#248
jeffsmale90 merged 17 commits into
mainfrom
feat/delegation-provider

Conversation

@jeffsmale90
Copy link
Copy Markdown
Collaborator

@jeffsmale90 jeffsmale90 commented May 25, 2026

📝 Description

Adds createx402DelegationProvider and related utils and types to @metamask/smart-accounts-kit/experimental.

The resulting DelegationProvider can be used with the x402Erc7710Client.

Examples

Minimal provider (quick start)

Use this when you just want the specified account to create a root delegation to cover the PaymentRequirements.

import { privateKeyToAccount } from 'viem/accounts';
import { createx402DelegationProvider } from '@metamask/smart-accounts-kit/experimental';
import { x402Erc7710Client } from '@metamask/x402';

const account = privateKeyToAccount(privateKey);

const erc7710Client = new x402Erc7710Client({
  delegationProvider: createx402DelegationProvider({
    account,
  }),
});

Re-delegate from a static permissionContext

Use this when you already have a parent delegation and want to redelegate it.

import { createx402DelegationProvider } from '@metamask/smart-accounts-kit/experimental';

const parentPermissionContext =
  '0x...'; // previously stored/issued permission context

const delegationProvider = createx402DelegationProvider({
  account,
  parentPermissionContext,
});

Add a short expiration

Use this for safer, short-lived delegations (for example, 60 seconds).

import { createx402DelegationProvider } from '@metamask/smart-accounts-kit/experimental';

const delegationProvider = createx402DelegationProvider({
  account,
  expirySeconds: 60
});

Or use a dynamic expiration, specifying a shorter expiration for larger amounts.

import { createx402DelegationProvider } from '@metamask/smart-accounts-kit/experimental';

const delegationProvider = createx402DelegationProvider({
  account,
  expirySeconds: (paymentRequirements) => {
    const shortLived = BigInt(requirements.amount) > 1_000_000n;
    return shortLived ? 20 : 60;
  }
});

Dynamic caveats per payment requirements

Use this when caveats should depend on network, asset, amount, etc.

import {
  CaveatType,
  createx402DelegationProvider,
} from '@metamask/smart-accounts-kit/experimental';

const delegationProvider = createx402DelegationProvider({
  account,
  environment,
  caveats: async (requirements) => {
    const now = Math.floor(Date.now() / 1000);

    // Example: tighter expiry for larger amounts
    const shortLived = BigInt(requirements.amount) > 1_000_000n;

    return [
      {
        type: CaveatType.Timestamp,
        afterThreshold: now - 5,
        beforeThreshold: now + (shortLived ? 20 : 60),
      },
      // add additional caveats here based on requirements.network/asset/payTo/etc.
    ];
  },
});

5) Use a dynamic parent permissionContext

Use this when parent delegations are stored by chain/user/merchant and loaded at runtime.

import { createx402DelegationProvider } from '@metamask/smart-accounts-kit/experimental';

const delegationProvider = createx402DelegationProvider({
  account,
  environment,
  parentPermissionContext: async (requirements) => {
    return await loadPermissionContext({
      network: requirements.network,
      payTo: requirements.payTo,
      asset: requirements.asset,
    });
  },
});

6) Deterministic from and salt for reproducibility

Use this when you want stable delegation derivation, or if delegating from an account other than the specified account (i.e., a MultiSig account where account is an authorized signer).

import { getAddress } from 'viem';
import { createx402DelegationProvider } from '@metamask/smart-accounts-kit/experimental';

const delegationProvider = createx402DelegationProvider({
  account,
  environment,
  from: getAddress('0x1234...'), // explicit delegator
  salt: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
});

🚀 Why?

The x402Erc7710Client must be instantiated with a DelegationProvider. Presently the caller must implement the logic for creating the DelegationPayload.

With this new x402DelegationProvider, the caller can model most use cases without having to implement complex delegation creation and signing. The intention is to cover the vast majority of use cases, while also allowing the caller to implement the DelegationProvider themselves if needed.

⚠️ Breaking Changes

List any breaking changes:

  • No breaking changes
  • Breaking changes (describe below):

📋 Checklist

Check off completed items:

  • Code follows the project's coding standards
  • Self-review completed
  • Documentation updated (if needed)
  • Tests added/updated
  • Changelog updated (if needed)
  • All CI checks pass

🔗 Related Issues

Link to related issues:
Closes #
Related to #

📚 Additional Notes

Any additional information, concerns, or context:


Note

Medium Risk
New delegation creation and signing paths with automatic caveat enforcement affect payment authorization scope; logic is security-sensitive but isolated to experimental exports and covered by extensive tests.

Overview
Adds an experimental createx402DelegationProvider under @metamask/smart-accounts-kit/experimental so x402Erc7710Client callers can supply a ready-made delegation provider instead of building signed delegation payloads themselves.

For each x402 PaymentRequirements challenge, the provider resolves static or deferred config (account, environment, caveats, parent permissionContext, from/salt, redeemer rules, optional expirySeconds), builds an open delegation scoped to the payment (ERC-20 transfer amount, payee, network), auto-appends redeemer, payee (AllowedCalldataEnforcer), and optional timestamp caveats when existing caveats or parent delegations are not strict enough, signs via signTypedData, and returns delegationManager, encoded permissionContext, and delegator.

Also exports generateSalt from @metamask/smart-accounts-kit/utils (32-byte crypto.getRandomValues), adds @metamask/utils for CAIP eip155 chain parsing (parseEip155ChainId), and documents both in the changelog. Broad unit tests cover the provider and caveat/context helpers.

Reviewed by Cursor Bugbot for commit ce66c3e. Bugbot is set up for automated code reviews on this repo. Configure here.

- if no facilitatorAddresses are specified, at least one redeemer caveat must be either in the parentPermissionContext or specified caveats
- if facilitatorAddresses are specified, only add a redeemer caveat if the redeemers are not already constrained to the facilitator addresses
- only if there isn't an existing caveat constraining the payee
- also minor refactor to put various x402 utilities into x402DelegationProviderUtils.ts
- also refactor types into x402DelegationProviderTypes.ts to simplify internal type dependencies
@jeffsmale90 jeffsmale90 force-pushed the feat/delegation-provider branch from 4611795 to 22f61b9 Compare May 25, 2026 23:05
@jeffsmale90 jeffsmale90 changed the title Add createDelegationProvider to @metamask/smart-accounts-kit/experimental Add createx402DelegationProvider to @metamask/smart-accounts-kit/experimental May 26, 2026
@jeffsmale90 jeffsmale90 marked this pull request as ready for review May 26, 2026 03:34
@jeffsmale90 jeffsmale90 requested a review from a team as a code owner May 26, 2026 03:34
throw new Error('Unsupported chain namespace');
}

return parseInt(reference, 10);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseInt can return NaN for non-numeric references (e.g. "eip155:testnet"). consider adding a Number.isFinite check and throwing if invalid.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call.

I've changed this to use Number(reference) and added an isNaN check.

existingDelegations,
expirySeconds,
}: EnsureExpirySufficientlyConstrainedParams): Caveat[] => {
const beforeThreshold = Math.floor(Date.now() / 1000) + expirySeconds;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add validation here to enforce expirySeconds > 0? Even though it’s caller-provided config and 0/negative values are unlikely in practice, it might still be safer to guard against them.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes!

}: EnsurePayeeSufficientlyConstrainedParams): Caveat[] => {
const allowedCalldataTerms = createAllowedCalldataTerms({
startIndex: TRANSFER_PAYEE_INDEX,
value: payee,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the payee here is a 20 byte address. ThecreateAllowedCalldataTerms does not ABI-pad the value, so for transfer(address,uint256) we either need to match the 20-byte address at offset 16, or pass a padded address slot at offset 4.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oof yeah, good catch!

…traint is required, and which addresses should be constrained
- pad payee address to 32 bytes for allowedCalldataTerms
- require expiry to be positive number
- reject invalid chainId in network string
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ce66c3e. Configure here.

@jeffsmale90 jeffsmale90 requested a review from mj-kiwi May 26, 2026 23:57
Copy link
Copy Markdown
Contributor

@mj-kiwi mj-kiwi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jeffsmale90 jeffsmale90 merged commit 327672d into main May 27, 2026
18 checks passed
@jeffsmale90 jeffsmale90 deleted the feat/delegation-provider branch May 27, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants