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:
- Keyless AI agents — software agents running in sandboxed environments where key storage is
impractical or undesirable. - 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. - Micropayment-gated APIs — HTTP services that accept X402 payments and need to translate
them into on-chain actions without intermediate key escrow. - 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:
- Payment — The payer transfers tokens to the forwarder (or to the forwarder’s designated
escrow) as an atomic ERC-3009transferWithAuthorizationor equivalent mechanism. - 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. - Relay — The forwarder calls the destination contract. The destination contract reads the
authenticated payer address viapgtrSender()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:
- Maintain a set of trusted forwarder addresses:
mapping(address => bool) public trustedForwarders; - Expose
isTrustedForwarder(address)returningtrustedForwarders[addr]. - When the effective caller needs to be determined, check
msg.senderagainsttrustedForwarders.
If trusted, read the authenticated payer viaIPGTRForwarder(msg.sender).pgtrSender()instead of
usingmsg.senderdirectly:function _effectiveSender() internal view returns (address) { if (trustedForwarders[msg.sender]) { return IPGTRForwarder(msg.sender).pgtrSender(); } return msg.sender; } - NOT use
msg.senderas an authorization check for requester-only operations when called through
a forwarder. The forwarder MUST setpgtrSender()to the correct requester before forwarding.
Forwarder Requirements
A PGTR forwarder MUST:
- Implement
IPGTRForwarderand returntruefromsupportsInterface(type(IPGTRForwarder).interfaceId). - Atomically execute payment transfer and destination call within a single transaction. If either
fails, the entire transaction MUST revert. - 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; - Verify the receipt has not expired before forwarding:
require(block.timestamp <= expiry, "Receipt expired"); - Set
pgtrSender()to the authenticated payer address for the duration of the forwarded call,
and reset it after the call returns or reverts. - Emit
PaymentGatedCallafter a successful forward.
A PGTR forwarder SHOULD:
- Be a multisig or governance-controlled address when used in production. Single-EOA forwarders
MUST be disclosed as a centralization risk. - Implement a configurable minimum payment amount per target+selector combination.
- 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 principalamount— payment amount in base token unitsnonce— 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 rejectedtarget— destination contract addressselector— 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:
| Dimension | ERC-2771 | PGTR |
|---|---|---|
| Actor model | Key-holding principals only | Actor-agnostic (human, agent, IoT, contract) |
| Authorization proof | Off-chain ECDSA signature | On-chain payment receipt |
| Principal requirement | Must hold a private key | Must hold tokens |
| Sender detection | Appended 20-byte suffix in calldata | pgtrSender() call on forwarder |
| Gas payment | Relayer pays gas | Forwarder (server) pays gas |
| Replay protection | Signature nonce in forwarder | Receipt hash in forwarder |
| Use case | Gas abstraction for key holders | Key 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:
- Simpler to implement — no calldata manipulation required.
- More auditable — the authentication path is explicit and searchable.
- Composable — any function can call
pgtrSender()onmsg.senderwithout 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:
- Have access to a private key.
- 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
- Receipt expiry windows — Short expiry windows (~60–300 seconds) limit the exploit window
if a receipt is intercepted in transit. - Per-selector minimum amounts — Set minimum payment thresholds per
(target, selector)pair
to make replaying receipts economically unattractive. - 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. - 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 here: GitHub - daydreamsai/taskmarket-contracts: Task Market Protocol — Solidity interfaces, reference implementation, and EIP drafts (TMP/PGTR) · GitHub
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
- Reentrancy — Forwarders MUST use a reentrancy guard around the forwarded call. Destination
contracts MUST use a reentrancy guard for all state-modifying operations. - Receipt replay — Forwarders MUST consume receipts atomically within the transaction that
forwards the call. Receipts MUST NOT be consumable across chains (includechainIdin the hash
if cross-chain receipt portability is a concern). - Forwarder trust boundary — Destination contracts MUST NOT add forwarders without governance
controls. An unrestrictedaddForwarderis functionally equivalent toselfdestruct. - pgtrSender() atomicity — The forwarder MUST reset
pgtrSender()after the forwarded call
returns. If the forwarder allows reentrant calls that readpgtrSender(), the previous payer
address may be incorrectly attributed to the reentrant call.
Copyright
Copyright and related rights waived via CC0.