--- eip: 8026 title: Data Callback Capability description: A way for apps to request user data from wallets author: Lukas Rosario (@lukasrosario), Arjun Dureja (@arjun-dureja), Spencer Stock (@spencerstock), Cody Crozier (@wcrozier12), Conner Swenberg (@ilikesymmetry), Sam Luo (@sluo10), Wilson Cusack (@wilsoncusack), Jake Feldman (@jakefeldman), Bruno Barbieri (@brunobar79) discussions-to: https://ethereum-magicians.org/t/erc-8026-data-callback-capability/25496 status: Draft type: Standards Track category: ERC created: 2025-09-17 requires: 5792 --- ## Abstract This proposal defines an [EIP-5792](./eip-5792.md) capability that allows apps to request user data. Additionally, this proposal specifies how wallets communicate with an app-provided callback URL so apps can check users' information and optionally modify transaction requests before users approve them. ## Motivation Apps often need user data such as contact and shipping information as part of a transaction request. Today, there is no standard for: - How apps describe the data they need from wallets. - How apps can validate user information before allowing them to proceed with a transaction (for example, ensuring a merchant can ship to a user's address before allowing them to complete a payment). - How wallets provide app-requested information, both during pre-signing validation and after a transaction request has been signed and submitted by a user. - How apps can update transaction requests based on user-provided information (for example, updating a payment request to include a shipping fee). This capability provides a way for apps and wallets to communicate about this information. ## Specification One new [EIP-5792](./eip-5792.md) wallet capability is defined. ### `dataCallback` Capability The `dataCallback` capability is implemented by both apps and wallets. #### App → Wallet Apps include the capability when invoking `wallet_sendCalls`. Capability object shape: ```typescript type DataCallbackType = | "email" | "phoneNumber" | "physicalAddress" | "name" | "onchainAddress"; type DataCallbackInfoRequest = { type: DataCallbackType; optional?: boolean; // default false }; type DataCallbackEventType = "initiated" | "preSign" | "postSign"; type DataCallbackEvent = | { type: "initiated"; context?: Record; // arbitrary app-defined object } | { type: "preSign"; requests: DataCallbackInfoRequest[]; // MUST be non-empty, no duplicate types context?: Record; // arbitrary app-defined object } | { type: "postSign"; context?: Record; // arbitrary app-defined object }; type DataCallbackCapability = { callbackURL: string; // MUST be HTTPS events: DataCallbackEvent[]; // MUST be non-empty }; ``` Constraints: - `events` MUST be present and non-empty. - Each event's `type` MUST be one of exactly `"initiated"`, `"preSign"`, or `"postSign"`. Wallets MUST reject unknown event names. - `callbackURL` MUST be a valid HTTPS URL. - `requests` is only permitted on `preSign` events and MUST be a non-empty array without duplicate `type` entries. - Each `optional` MUST be a boolean if present (defaults to false). - If `type` is `onchainAddress`, it MUST NOT be marked optional. - If a `preSign` event with `requests` is desired, a `preSign` event MUST be present in `events` (there is no default). `events` MUST NOT be omitted. Examples: ```json { "capabilities": { "dataCallback": { "callbackURL": "https://example.com/callback", "events": [ { "type": "initiated", "context": { "orderId": "123" } }, { "type": "preSign", "requests": [ { "type": "email" }, { "type": "physicalAddress", "optional": true } ], "context": { "shippingTier": "express" } }, { "type": "postSign", "context": { "webhookTag": "after" } } ] } } } ``` #### Wallet → Service (HTTP Interface as Webhook Events) If `dataCallback.events` is present, the wallet MUST send HTTPS POST requests to `callbackURL` for each event listed, using the JSON bodies defined below. The wallet MAY first collect missing data via its UI or user profile when handling `preSign`. Users MAY intentionally leave optional fields blank; wallets MUST omit intentionally blank fields from `requestedInfo`. Shared types: ```typescript type RequestedInfo = { email?: string; name?: { firstName: string; lastName: string }; onchainAddress?: `0x${string}`; physicalAddress?: { address1: string; address2?: string; city: string; state?: string; postalCode: string; countryCode: string; // ISO 3166-1 alpha-2 name?: { firstName: string; familyName: string }; }; phoneNumber?: { number: string; country: string }; // E.164 number and ISO country code }; type DataCallbackRequestEcho = { capabilities: Record; // app-provided capabilities (may be updated by service during preSign only) calls: { to: `0x${string}`; data?: `0x${string}`; value?: `0x${string}`; }[]; chainId: `0x${string}`; // hex string (e.g., '0x1') }; ``` Event payloads (wallet → `callbackURL`): ```typescript // Initiated (read-only notification) type DataCallbackInitiatedEvent = { event: "initiated"; context?: Record; request: DataCallbackRequestEcho; // original values }; // Pre-sign (allows validation and mutation) type DataCallbackPreSignEvent = { event: "preSign"; context?: Record; requestedInfo: RequestedInfo; // only keys requested and available are included request: DataCallbackRequestEcho; // current values }; // Post-sign (read-only notification) type DataCallbackPostSignEvent = { event: "postSign"; context?: Record; request: DataCallbackRequestEcho; // final values used when submitted finalRequestedInfo?: RequestedInfo; // final user info used (if preSign requested info) callsId: string; // EIP-5792 submission result identifier }; ``` Service responses (service → wallet): ```typescript // Field-level validation errors for preSign type DataCallbackErrors = { email?: string; phoneNumber?: { country?: string; number?: string }; physicalAddress?: { address1?: string; address2?: string; city?: string; state?: string; postalCode?: string; country?: string; }; name?: { firstName?: string; lastName?: string }; onchainAddress?: string; }; // Mutation-only for preSign; body is ignored for other events type DataCallbackPreSignResponse = { request: { calls: { to: `0x${string}`; data?: `0x${string}`; value?: `0x${string}`; }[]; // updated (or original) calls capabilities?: Record; // updated (or original) capabilities (MUST NOT modify dataCallback) chainId?: `0x${string}`; // updated (or original) chain id }; errors?: DataCallbackErrors; // optional validation messages per field }; ``` Notes: - For `initiated` and `postSign`, responses are acknowledgment-only: wallets MUST ignore any response body and MUST NOT apply mutations. - For `preSign`, services MAY return `errors` to indicate field-level validation issues. Wallets SHOULD prompt users to correct fields and MAY re-invoke the `preSign` event after user correction. - For `preSign`, services MUST return either original or updated request fields (calls, capabilities, chain id). If no updates are required, services SHOULD return original values. - Services MAY modify capabilities during `preSign`, but they MUST NOT modify the `dataCallback` capability. I.e., the `dataCallback` capability value MUST stay constant throughout the request flow. - Wallets SHOULD retry non-2xx responses for `initiated` and `postSign` up to 3 retries using exponential backoff with jitter (e.g., ~1s, ~3s, ~9s; cap 10s). Wallets MUST NOT retry `preSign` automatically; they SHOULD retry only after user input changes or service availability is restored. #### Wallet Behavior - Wallets MUST indicate support for this capability per chain in their `wallet_getCapabilities` response. - When `dataCallback.events` includes `initiated`, wallets SHOULD POST the `initiated` event promptly after receiving `wallet_sendCalls` with `dataCallback`, before any signing UI. - When `dataCallback.events` includes `preSign`, wallets MUST collect the requested information, POST the `preSign` event before finalizing the user operation, and honor any returned mutations. If `errors` are present, wallets MUST NOT let users proceed until errors are resolved or the user cancels. - Wallets MUST only send keys in `requestedInfo` that were requested and available (after user consent). If the user intentionally leaves an optional field blank, wallets MUST omit it from `requestedInfo`. - Wallets MUST pass the current `calls`, `capabilities`, and `chainId` in each event payload and, for `preSign` only, MUST accept updated `calls`/`capabilities`/`chainId` from the response. - When `dataCallback.events` includes `postSign`, wallets SHOULD POST the `postSign` event after the user has approved and the request has been submitted. - When `dataCallback.events` includes `postSign`, wallets SHOULD POST the `postSign` event after the user has approved and the request has been submitted, and MUST include the resulting `callsId` as returned by EIP-5792. - When `dataCallback.events` includes `postSign`, wallets SHOULD POST the `postSign` event after the user has approved and the request has been submitted, and MUST include the resulting `callsId` as returned by EIP-5792. If a `preSign` event with `requests` occurred, wallets MUST include `finalRequestedInfo` mirroring the `capabilities.dataCallback` augmentation returned from `wallet_sendCalls`. - Wallets MUST reject `dataCallback` requests that violate the constraints above (e.g., missing `events`, unknown event type, `requests` provided outside `preSign`, non-HTTPS `callbackURL`). #### Wallet → App: Final user information in `wallet_sendCalls` response If a `dataCallback` capability is present in the request, wallets MUST include the final user information they used under `capabilities.dataCallback` in the `wallet_sendCalls` response. The value MUST reflect the final set of keys used (respecting omitted optional fields). ```typescript type WalletSendCallsResponseAugmentation = { capabilities?: { dataCallback?: RequestedInfo; // final user info provided }; }; ``` ##### Example ```json { "id": "0x...", "capabilities": { "dataCallback": { "email": "hello@example.com" } } } ``` #### `wallet_getCapabilities` Response ```typescript type DataCallbackCapabilitySupport = { supported: boolean; }; ``` Example: ```json { "0x1": { "dataCallback": { "supported": true } }, "0x2105": { "dataCallback": { "supported": true } } } ``` ## Rationale - HTTPS-only callback URLs minimize the risk of data interception or downgrade attacks. - Modeling the capability as explicit webhook events (`initiated`, `preSign`, `postSign`) aligns with familiar integration patterns and clarifies when mutation is allowed (only during `preSign`). - The request/response shapes are narrowly scoped to the data types wallets commonly manage, and the transformation rules avoid leaking internal profile constructs. - `onchainAddress` is not meaningfully optional. Once a user submits a transaction, the app can infer the user's onchain address by polling the resulting `callsId` and associated receipt; marking it optional would not prevent its disclosure. ### Backwards Compatibility - The capability is additive to [EIP-5792](./eip-5792.md) and does not change existing method semantics. ## Security Considerations - `callbackURL` MUST be HTTPS. Wallets MUST validate the scheme. - Apps are encouraged to use a backend they control as the `callbackURL` and proxy to third-party services if needed, avoiding leakage of service API keys. - Wallets MUST minimize the `requestedInfo` to only what was requested and consented to by the user and MUST only include `requestedInfo` in the `preSign` event. - Services SHOULD validate inputs and return clear field-level errors for `preSign` rather than failing the entire request. - For `initiated` and `postSign`, wallets MAY retry delivery up to 3 times on non-2xx responses (exponential backoff with jitter). Services SHOULD tolerate duplicate deliveries that can result from retries (no idempotency guarantee is provided by this spec). - Event `context` is application-defined and opaque to wallets. Services MUST treat `context` as untrusted input and validate/sanitize accordingly. ### Privacy Considerations - Wallets SHOULD provide UI to review and optionally omit optional fields. Omitted optional fields MUST NOT be sent. - Wallets MUST NOT include `requestedInfo` in `initiated` or `postSign` events. - Wallets MUST NOT include `requestedInfo` in `initiated` events. In `postSign`, wallets MUST include `finalRequestedInfo` only when a `preSign` with `requests` was performed. - Apps should avoid including personally identifiable information in `context`. If included, it is subject to the app's own privacy policies and data handling obligations. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).