EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8194: Payment-Gated Transaction Relay (PGTR)

On-chain payment receipt as actor-agnostic authorization for any token-holding principal without requiring a private key

Authors
Created2026-03-10
Discussion Linkhttps://ethereum-magicians.org/t/erc-8194-payment-gated-transaction-relay/27934
Requires
Referenced by

Markdown

https://raw.githubusercontent.com/beauwilliams/ERC...

EIP-GPT summary

Contents
AbstractMotivationSpecificationOverviewIPGTRForwarder InterfaceDestination Contract RequirementsForwarder RequirementsReplay ProtectionERC-165 Interface DetectionRelationship to ERC-2771RationaleWhy pgtrSender() instead of appended calldata?Why payment receipt instead of signed authorization?Why mapping(address => bool) trustedForwarders instead of single address?Security Considerations for Forwarder OperatorsBackwards CompatibilityReference ImplementationSecurity ConsiderationsCopyright

Abstract

Payment-Gated Transaction Relay (PGTR) defines a protocol for forwarding on-chain transactions on behalf of principals β€” humans, AI agents, IoT devices, or contracts β€” where the authorization proof is an on-chain payment receipt rather than a cryptographic signature. A PGTR Forwarder accepts an X402-style payment from a payer, verifies the payment against an expected amount and target, and calls the destination contract with the payer's address available as the authenticated sender. Destination contracts read the authenticated payer address from the forwarder via pgtrSender() rather than from msg.sender.

PGTR is actor-agnostic: the payer may be a human using a wallet app, an autonomous AI agent with a funded address, an IoT device, or any other token-holding entity. The protocol treats all principals identically β€” authorization is "I paid", not "I signed". This is the property that allows downstream smart contracts to support human↔agent, agent↔agent, and human↔human task interactions under a single interface.

PGTR is a new primitive. It is not a profile of ERC-2771. ERC-2771 solves gas abstraction β€” the signer still needs a key and must produce an off-chain signature before the relay can act. PGTR solves key abstraction β€” any actor that controls tokens and can reach an HTTP endpoint can authorize on-chain actions without ever managing a private key.

Motivation

Any actor β€” human or machine β€” that can hold tokens should be able to authorize on-chain actions without the overhead of private key management. Existing meta-transaction standards (ERC-2771, ERC-4337) presuppose that the principal can produce a valid cryptographic signature. This assumption breaks for a wide class of actors:

  1. Keyless AI agents β€” software agents running in sandboxed environments where key storage is impractical or undesirable.
  2. Human users preferring payment-only UX β€” users who interact via payment flows (e.g., credit card β†’ stablecoin bridge β†’ X402) and should not be required to manage a separate signing key for on-chain authorization.
  3. Micropayment-gated APIs β€” HTTP services that accept X402 payments and need to translate them into on-chain actions without intermediate key escrow.
  4. IoT and machine accounts β€” devices and automated systems where authorization is "I paid" rather than "I signed."

PGTR unifies all these cases under a single primitive: any token-holding actor, regardless of whether they are human or machine, can participate in PGTR-gated protocols on equal footing.

PGTR establishes a minimal interface that destination contracts implement to trust PGTR forwarders, and that forwarders implement to be detectable by ERC-165.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Overview

The PGTR flow proceeds in three steps:

  1. Payment β€” The payer transfers tokens to the forwarder (or to the forwarder's designated escrow) as an atomic ERC-3009 transferWithAuthorization or equivalent mechanism.
  2. Verification β€” The forwarder verifies: (a) the payment amount meets the threshold for the requested action; (b) the payment receipt has not been consumed before; (c) the receipt has not expired.
  3. Relay β€” The forwarder calls the destination contract. The destination contract reads the authenticated payer address via pgtrSender() on the forwarder.

IPGTRForwarder Interface

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

/// @title IPGTRForwarder β€” PGTR Forwarder Interface
/// @notice Implemented by PGTR forwarder contracts. Destination contracts MUST check
///         isTrustedForwarder(msg.sender) before reading pgtrSender().
interface IPGTRForwarder is IERC165 {
    /// @notice Returns true. Allows ERC-165 detection of PGTR forwarders.
    function isPGTRForwarder() external view returns (bool);

    /// @notice The payer address that authorized the current forwarded call.
    ///         Analogous to ERC-2771's appended msg.sender.
    ///         MUST revert if called outside of an active forwarded call.
    function pgtrSender() external view returns (address payer);

    /// @notice Returns true if addr is trusted as a PGTR forwarder by this contract.
    ///         Mirrors the ERC-2771 recipient-side pattern for symmetrical detection.
    function isTrustedForwarder(address addr) external view returns (bool);

    /// @notice Emitted each time a payment-gated call is forwarded successfully.
    /// @param payer          The authenticated payer address.
    /// @param target         The destination contract that received the call.
    /// @param selector       The 4-byte function selector of the forwarded call.
    /// @param paymentAmount  The token amount paid (in the payment token's base units).
    event PaymentGatedCall(
        address indexed payer,
        address indexed target,
        bytes4  indexed selector,
        uint256         paymentAmount
    );
}

Destination Contract Requirements

A contract that accepts forwarded calls from a PGTR forwarder MUST:

  1. Maintain a set of trusted forwarder addresses:
    mapping(address => bool) public trustedForwarders;
  2. Expose isTrustedForwarder(address) returning trustedForwarders[addr].
  3. When the effective caller needs to be determined, check msg.sender against trustedForwarders. If trusted, read the authenticated payer via IPGTRForwarder(msg.sender).pgtrSender() instead of using msg.sender directly:
    function _effectiveSender() internal view returns (address) {
        if (trustedForwarders[msg.sender]) {
            return IPGTRForwarder(msg.sender).pgtrSender();
        }
        return msg.sender;
    }
  4. NOT use msg.sender as an authorization check for requester-only operations when called through a forwarder. The forwarder MUST set pgtrSender() to the correct requester before forwarding.

Forwarder Requirements

A PGTR forwarder MUST:

  1. Implement IPGTRForwarder and return true from supportsInterface(type(IPGTRForwarder).interfaceId).
  2. Atomically execute payment transfer and destination call within a single transaction. If either fails, the entire transaction MUST revert.
  3. Before forwarding, verify the payment receipt has not been consumed:
    mapping(bytes32 => bool) public consumedReceipts;
    
    bytes32 receiptHash = keccak256(abi.encode(
        payer, amount, nonce, expiry, target, selector
    ));
    require(!consumedReceipts[receiptHash], "Receipt already consumed");
    consumedReceipts[receiptHash] = true;
  4. Verify the receipt has not expired before forwarding:
    require(block.timestamp <= expiry, "Receipt expired");
  5. Set pgtrSender() to the authenticated payer address for the duration of the forwarded call, and reset it after the call returns or reverts.
  6. Emit PaymentGatedCall after a successful forward.

A PGTR forwarder SHOULD:

  1. Be a multisig or governance-controlled address when used in production. Single-EOA forwarders MUST be disclosed as a centralization risk.
  2. Implement a configurable minimum payment amount per target+selector combination.
  3. Verify that the payer's payment token allowance or signed authorization covers the required amount before proceeding.

Replay Protection

Payment receipts MUST be single-use. The canonical receipt hash is:

keccak256(abi.encode(payer, amount, nonce, expiry, target, selector))

Where:

  • payer β€” address of the paying principal
  • amount β€” payment amount in base token units
  • nonce β€” unique value per payer to prevent replay (MAY be a random bytes32 or a monotonic counter)
  • expiry β€” Unix timestamp after which the receipt MUST be rejected
  • target β€” destination contract address
  • selector β€” 4-byte function selector of the intended call

The forwarder MUST store consumed receipt hashes and reject any duplicate.

ERC-165 Interface Detection

The IPGTRForwarder interfaceId is the XOR of the selectors of isPGTRForwarder(), pgtrSender(), and isTrustedForwarder(address). Compute with type(IPGTRForwarder).interfaceId in Solidity.

Relationship to ERC-2771

PGTR and ERC-2771 are different primitives solving different problems:

DimensionERC-2771PGTR
Actor modelKey-holding principals onlyActor-agnostic (human, agent, IoT, contract)
Authorization proofOff-chain ECDSA signatureOn-chain payment receipt
Principal requirementMust hold a private keyMust hold tokens
Sender detectionAppended 20-byte suffix in calldatapgtrSender() call on forwarder
Gas paymentRelayer pays gasForwarder (server) pays gas
Replay protectionSignature nonce in forwarderReceipt hash in forwarder
Use caseGas abstraction for key holdersKey abstraction for any token-holding actor

A destination contract MAY support both ERC-2771 and PGTR simultaneously by checking whether msg.sender is an ERC-2771 trusted forwarder first, then a PGTR trusted forwarder.

PGTR does NOT claim to be a "profile" of ERC-2771. It uses a different sender-detection mechanism (explicit function call vs. appended calldata bytes) and a fundamentally different authorization model (payment verification vs. signature verification). Implementations MUST NOT claim compliance with ERC-2771 when implementing only IPGTRForwarder.

Rationale

Why pgtrSender() instead of appended calldata?

ERC-2771 appends the original sender as the last 20 bytes of calldata. This approach requires the destination contract to parse calldata in every function that needs the authenticated sender. PGTR uses an explicit pgtrSender() view call on the forwarder, which is:

  1. Simpler to implement β€” no calldata manipulation required.
  2. More auditable β€” the authentication path is explicit and searchable.
  3. Composable β€” any function can call pgtrSender() on msg.sender without calldata size constraints.

The trade-off is one additional external STATICCALL per invocation (~700 gas at current pricing), which is negligible relative to the cost of the forwarded operation.

Why payment receipt instead of signed authorization?

A signed authorization requires the principal to:

  1. Have access to a private key.
  2. Compute and transmit a valid signature before calling the relay.

This requirement is incompatible with lightweight keyless agents operating in constrained environments. Payment receipts require only that the principal hold tokens and reach a payment endpoint β€” requirements met by any autonomous agent with a funded wallet address.

Why mapping(address => bool) trustedForwarders instead of single address?

A single trusted forwarder address is a single point of failure. If the forwarder EOA is compromised or becomes unavailable, the destination contract is bricked. A mapping allows:

  • Key rotation without contract redeployment.
  • Multiple parallel forwarders for availability.
  • Staged transitions (add new, remove old) without downtime.

Security Considerations for Forwarder Operators

  1. Receipt expiry windows β€” Short expiry windows (~60–300 seconds) limit the exploit window if a receipt is intercepted in transit.
  2. Per-selector minimum amounts β€” Set minimum payment thresholds per (target, selector) pair to make replaying receipts economically unattractive.
  3. Frontrunning β€” Because receipt hashes include the payer address, frontrunning a relay call requires the attacker to also control the payment token transfer. Atomic execution within a single transaction mitigates frontrunning entirely.
  4. Forwarder compromise β€” A compromised forwarder can relay arbitrary calls on behalf of payers who previously paid. Operators SHOULD use a multisig or on-chain governance key as the forwarder owner. Destinations SHOULD emit events for all forwarded calls so monitoring can detect anomalies.

Backwards Compatibility

This ERC introduces a new interface with no modifications to existing standards. Destination contracts that implement this standard alongside ERC-2771 remain compatible with ERC-2771 relayers, provided they check forwarder types in priority order.

Reference Implementation

See src/interfaces/IPGTRForwarder.sol in the reference implementation repository.

A complete reference implementation of a PGTR-compatible destination contract is provided in TaskMarket.sol, which implements the trustedForwarders mapping pattern and exposes supportsInterface(type(IPGTRForwarder).interfaceId) for ERC-165 detection.

The IPGTRForwarder interface ID is the XOR of the 4-byte Keccak256 selectors of all functions declared in the interface (isPGTRForwarder(), pgtrSender(), isTrustedForwarder(address)). Compute in Solidity with type(IPGTRForwarder).interfaceId.

Security Considerations

  1. Reentrancy β€” Forwarders MUST use a reentrancy guard around the forwarded call. Destination contracts MUST use a reentrancy guard for all state-modifying operations.
  2. Receipt replay β€” Forwarders MUST consume receipts atomically within the transaction that forwards the call. Receipts MUST NOT be consumable across chains (include chainId in the hash if cross-chain receipt portability is a concern).
  3. Forwarder trust boundary β€” Destination contracts MUST NOT add forwarders without governance controls. An unrestricted addForwarder is functionally equivalent to selfdestruct.
  4. pgtrSender() atomicity β€” The forwarder MUST reset pgtrSender() after the forwarded call returns. If the forwarder allows reentrant calls that read pgtrSender(), the previous payer address may be incorrectly attributed to the reentrant call.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth