Here below is a SNIP proposal around standardizing the API between users and paymaster services. This proposal also affects SNIP-9 (outside execution): changes to SNIP-9 will be proposed and linked here as well when available. The current SNIP will refer to the changes in SNIP-9 when available. Link to the PR here.
snip: xxx
title: Applicative Paymaster API Standard
description:
author: Leonardo Lerer leo@starkware.co, AVNU
discussions-to:
status: Review
type: Standards Track
category: Core
created: 2025-02-05
Simple Summary
This SNIP proposes a standardized interface between an applicative paymaster service and a user. A user can be a single individual, a wallet, a DApp backend, etc… The specification aims to be as minimal as possible while enabling a full applicative paymaster flow with good UX. Any paymaster service is free to expand on this specification and add more features to it.
Motivation
The motivation of this SNIP is to make it easier for the Starknet ecosystem to integrate with an applicative paymaster. A neutral specification prevents fragmentation of interfaces of different paymaster services and simplifies paymaster integration for any entity that wishes to do so. It makes it possible for Starknet SDKs to integrate a paymaster specification which is independent of any existing vendor.
Specification
The full JSON-RPC specification is found in ../assets/snip-x/paymaster_api.json
. The specification is modeled around the following flow:
- A user submits to the paymaster service a list of calls that they wish to make, along with the token they wish to pay fees with.
- The paymaster service responds with a typed object (in the sense of SNIP-12) that wraps the calls submitted by the user and which is ready for signature. They SHOULD also send the amount of ERC-20 token required from the user and the exchange rate used (with respect to STRK). This means that the fee estimation logic is handled by the paymaster and not by the user. The purpose of the typed object is to:
- Wrap the user calls into a format compatible with outside execution (SNIP-9).
- Potentially add additional calls which are necessary for the fee accounting logic of the paymaster service.
- The user signs on the typed object and sends it (along with the signature) to the paymaster service for execution.
- The paymaster service finally sends a transaction onchain from its relayer account, containing the signed outside execution payload.
We note that the RPC specification does not impose the previous flow upon the user and paymaster, or impose limitations on the content of the typed data that the paymaster returns from the buildTypedData
endpoint. We expand more on this point in the Security Considerations section of this SNIP.
Below we list constant JSON objects that correspond to properties of some objects in the JSON RPC specification. These are the same as the ones that appear in SNIP-9 (outside execution) for its different versions, but we list them here as well for completeness.
Version 1
The properties types
, domain
and primaryType
of the object OUTSIDE_EXECUTION_TYPED_DATA_V1
MUST be set as follows:
"types": {
"StarkNetDomain": [
{ "name": "name", "type": "felt" },
{ "name": "version", "type": "felt" },
{ "name": "chainId", "type": "felt" },
],
"OutsideExecution": [
{ "name": "caller", "type": "felt" },
{ "name": "nonce", "type": "felt" },
{ "name": "execute_after", "type": "felt" },
{ "name": "execute_before", "type": "felt" },
{ "name": "calls_len", "type": "felt" },
{ "name": "calls", "type": "OutsideCall*" },
],
"OutsideCall": [
{ "name": "to", "type": "felt" },
{ "name": "selector", "type": "felt" },
{ "name": "calldata_len", "type": "felt" },
{ "name": "calldata", "type": "felt*" },
],
},
"domain": {
"name": "Account.execute_from_outside",
"version": "1",
"chainId": "0x534e5f4d41494e"
},
"primaryType": "OutsideExecution"
The "chainId"
property of the "domain"
object is set to the chainId of Starknet mainnet. For Starknet testnet, the corresponding chainId should be used.
Version 2
The properties types
, domain
and primaryType
object OUTSIDE_EXECUTION_TYPED_DATA_V2
MUST be set as follows:
"types": {
"StarknetDomain": [
{ "name": "name", "type": "shortstring" },
{ "name": "version", "type": "shortstring" },
{ "name": "chainId", "type": "shortstring" },
{ "name": "revision", "type": "shortstring" }
],
"OutsideExecution": [
{ "name": "Caller", "type": "ContractAddress" },
{ "name": "Nonce", "type": "felt" },
{ "name": "Execute After", "type": "u128" },
{ "name": "Execute Before", "type": "u128" },
{ "name": "Calls", "type": "Call*" }
],
"Call": [
{ "name": "To", "type": "ContractAddress" },
{ "name": "Selector", "type": "selector" },
{ "name": "Calldata", "type": "felt*" }
]
},
"domain": {
"name": "Account.execute_from_outside",
"version": "2",
"chainId": "0x534e5f4d41494e"
},
"primaryType": "OutsideExecution"
The field "revision"
of the "domain
object above is omitted: it should follow the revision of SNIP-12 that is being used (either the integer 1
or the shortstring "2"
).
The "chainId"
property of the "domain"
object is set to the chainId of Starknet mainnet. For Starknet testnet, the corresponding chainId should be used.
Version 3-rc
"types": {
"StarknetDomain": [
{ "name": "name", "type": "shortstring" },
{ "name": "version", "type": "shortstring" },
{ "name": "chainId", "type": "shortstring" },
{ "name": "revision", "type": "shortstring" }
],
"OutsideExecution": [
{ "name": "Caller", "type": "ContractAddress" },
{ "name": "Nonce", "type": "felt" },
{ "name": "Execute After", "type": "u128" },
{ "name": "Execute Before", "type": "u128" },
{ "name": "Calls", "type": "Call*" },
{ "name": "Fee", "type": "Fee Mode" }
],
"Call": [
{ "name": "To", "type": "ContractAddress" },
{ "name": "Selector", "type": "selector" },
{ "name": "Calldata", "type": "felt*" }
],
"Fee Mode": [
{ "name": "No Fee", "type": "()" },
{ "name": "Pay Fee", "type": "(FeeTransfer)" }
],
"Fee Transfer": [
{ "name": "Fee Amount", "type": "TokenAmount" },
{ "name": "Fee Receiver", "type": "ContractAddress" }
]
},
"domain": {
"name": "Account.execute_from_outside",
"version": "3",
"chainId": "0x534e5f4d41494e"
},
"primaryType": "OutsideExecution"
The field "revision"
of the "domain
object above is omitted: it should follow the revision of SNIP-12 that is being used (either the integer 1
or the shortstring "2"
).
The "chainId"
property of the "domain"
object is set to the chainId of Starknet mainnet. For Starknet testnet, the corresponding chainId should be used.
Backwards Compatibility
This specification may be breaking for existing paymaster services: more precisely, compliance with this specification may incur breaking upgrades for existing paymaster services.
Security Considerations
From the point of view of the user, the critical juncture during a “full” interaction with a paymaster (i.e. one that involves calling method buildTypedData
and then execute
) is the inspection of the typed data returned to them by the buildTypedData
endpoint. In case the user account is already deployed, the array of calls in this object SHOULD either
- Be exactly equal to the array of calls submitted by the user.
- The first call is a single
Transfer
call originating from the user account and calling the ERC-20 contract of the requested token, followed by the array of calls submitted by the user.
The user SHOULD parse the returned typed data according to option 1. or option 2. In case of option 2. the user should double-check locally that the Transfer
call involves the chosen ERC-20 contract and decide whether the amount is acceptable or not. Wallets and Dapps that are integrated with a paymaster should make this critical information as clear as possible to the user.
Copyright
Copyright and related rights waived via MIT.