Payment Attestations
Verify Naven-signed x402 payment facts onchain or offchain.
Naven issues an EIP-712 attestation for every indexed x402 payment. The attestation is a portable statement that Naven observed a payment with the specified payer, recipient, token, amount, and recipient sequence.
An attestation does not prescribe how a project uses a payment. Consumers choose their own fulfillment, authorization, replay protection, and finality policies.
Signed schema
The current schema is Naven Payment Attestation version 2:
struct PaymentAttestation {
bytes32 paymentId;
address payer;
address recipient;
uint256 recipientSequence;
address token;
uint256 amount;
}The EIP-712 domain contains:
{
"name": "Naven Payment Attestation",
"version": "2"
}The domain intentionally does not contain a verifying contract or destination chain. The same attestation can therefore be verified by offchain services and contracts on different networks.
Payment ID
paymentId uniquely identifies the indexed payment event:
paymentId = keccak256(
abi.encode(
"naven.payment.v1",
sourceChainId,
transactionHash,
logIndex
)
);The source chain, transaction hash, and log index are committed inside the
payment ID. Consumers can use the single bytes32 paymentId as their storage
and replay-protection key.
Recipient sequence
recipientSequence is assigned independently for each (sourceChainId, recipient) pair:
- The first indexed payment starts at
1. - Each later payment receives exactly the next number.
- A number is never reassigned or changed after issuance.
The sequence is included in the EIP-712 signature. It can be used as a compact,
recipient-specific ingestion cursor without replacing paymentId as the unique
payment identity. It records Naven issuance order, which is not necessarily the
same as source-chain transaction order.
Verify with viem
Install viem, fetch a transaction by payment ID, and verify the response
against the Naven signer address your application has explicitly allowlisted:
import { verifyTypedData } from "viem";
const trustedNavenSigner =
"0xc90fd9dbc7234e00b8160c53480e58ad973f547d";
const response = await fetch(
"https://api.naven.network/x402scan/transactions/0xPAYMENT_ID",
);
const transaction = await response.json();
const { attestation } = transaction;
if (transaction.signer.toLowerCase() !== trustedNavenSigner) {
throw new Error("Unexpected Naven attestation signer");
}
const valid = await verifyTypedData({
address: trustedNavenSigner,
domain: {
name: "Naven Payment Attestation",
version: "2",
},
primaryType: "PaymentAttestation",
types: {
PaymentAttestation: [
{ name: "paymentId", type: "bytes32" },
{ name: "payer", type: "address" },
{ name: "recipient", type: "address" },
{ name: "recipientSequence", type: "uint256" },
{ name: "token", type: "address" },
{ name: "amount", type: "uint256" },
],
},
message: {
paymentId: attestation.paymentId,
payer: attestation.payer,
recipient: attestation.recipient,
recipientSequence: BigInt(attestation.recipientSequence),
token: attestation.token,
amount: BigInt(attestation.amount),
},
signature: transaction.signature,
});
if (!valid) throw new Error("Invalid Naven payment attestation");Do not accept the signer field merely because it appears in the same API
response. Compare it with a signer address obtained from a trusted Naven source
or pinned in your application configuration.
Verify in Solidity
The domain has only name and version, so a contract can reproduce the
EIP-712 digest without knowing the payment's source chain or a designated
consumer contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
library NavenPaymentAttestations {
bytes32 internal constant DOMAIN_TYPEHASH =
keccak256("EIP712Domain(string name,string version)");
bytes32 internal constant NAME_HASH =
keccak256("Naven Payment Attestation");
bytes32 internal constant VERSION_HASH = keccak256("2");
bytes32 internal constant ATTESTATION_TYPEHASH = keccak256(
"PaymentAttestation(bytes32 paymentId,address payer,address recipient,uint256 recipientSequence,address token,uint256 amount)"
);
struct PaymentAttestation {
bytes32 paymentId;
address payer;
address recipient;
uint256 recipientSequence;
address token;
uint256 amount;
}
function recoverSigner(
PaymentAttestation calldata attestation,
bytes calldata signature
) internal pure returns (address) {
bytes32 domainSeparator = keccak256(
abi.encode(DOMAIN_TYPEHASH, NAME_HASH, VERSION_HASH)
);
bytes32 structHash = keccak256(
abi.encode(
ATTESTATION_TYPEHASH,
attestation.paymentId,
attestation.payer,
attestation.recipient,
attestation.recipientSequence,
attestation.token,
attestation.amount
)
);
bytes32 digest = keccak256(
abi.encodePacked("\x19\x01", domainSeparator, structHash)
);
return ECDSA.recover(digest, signature);
}
}Compare the recovered address with a Naven signer pinned by the consuming
contract. If an attestation authorizes a one-time action, store its paymentId
as consumed before completing that action.
Consumer responsibilities
Before fulfilling a purchase or granting access, a consumer should:
- Verify the EIP-712 signature against its allowlisted Naven signer.
- Check
recipient,token, andamountagainst its own requirements. - Decide whether the payment has sufficient source-chain finality.
- Store
paymentIdand reject reuse when its business rule is one payment per fulfillment.
Naven does not bind an attestation to a consumer contract, destination chain, order, entitlement, or expiration. Those rules remain under the consumer's control.