On-Chain Tracking - Adding an Identifier to Your Safe Accounts
Attribute active users and UserOperations to your project by tagging each userOp's callData with a 32-byte marker. Your indexer filters on the marker, no extra infrastructure, no off-chain correlation.
Generate an On-Chain Identifier
Pass onChainIdentifierParams when you initialize the Safe account. The SDK appends the marker to every userOp's callData.
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";
const smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress], {
onChainIdentifierParams: {
project: "YourProject", // required — the thing you key analytics off
platform: "Web", // optional — 'Web' | 'Mobile' | 'Safe App' | 'Widget'
tool: "abstractionkit", // optional — which SDK
toolVersion: "0.3.2", // optional — SDK version
},
});
Only project is required. platform, tool, and toolVersion refine attribution (Web vs Mobile, which SDK, which version). Use the same values everywhere, analytics key off this exact string.
Already-deployed accounts
For accounts that already exist on-chain, pass the same params to the constructor. New userOps will carry the marker from that point on; historical userOps are not retroactively tagged.
const smartAccount = new SafeAccount(accountAddress, {
onChainIdentifierParams: { project: "YourProject" },
});
Getting the On-Chain Identifier
const onchainIdentifier = smartAccount.onChainIdentifier;
// "0x5afe00..."
View the identifier generation in the generateOnChainIdentifier source code.
Indexer patterns
Exact match: UserOperationEvent
The EntryPoint emits a UserOperationEvent log for every included userOp. Decode it, pull the userOp's callData, and check the suffix. This is the right approach: it's per-userOp and the marker sits exactly at the tail.
const endsWithId = userOp.callData
.toLowerCase()
.endsWith(identifier.toLowerCase());
Aggregate:
- Unique
sendervalues → active users - Total matching events → userOp volume
- Group by the identifier's trailing hashes → split by platform / tool / version
Fuzzy match: handleOps tx input
The bundler wraps userOps in EntryPoint.handleOps(ops[], beneficiary). The ABI-encoded tx calldata is laid out as:
selector (4 bytes)
│
├─ head
│ offset-to-ops (32 bytes)
│ beneficiary (32 bytes)
│
└─ ops data (dynamic: length + each op, with callData inlined as dynamic bytes)
The marker is inside each op's callData, so it appears somewhere in the dynamic tail of the tx input, not strictly at the end. With multiple userOps batched into one handleOps call, each op's callData contributes its own marker occurrence at a different offset. Suffix matching on tx.input is not reliable. Use substring match, or decode the ops array and inspect each callData:
const tagged = tx.input
.toLowerCase()
.includes(identifier.toLowerCase());
Good enough for quick dashboards, but batches of multiple userOps in one handleOps call collapse into one match. For per-userOp attribution, decode the ops array (ABI-decode the tx input) and check each op's callData individually, or prefer the event-based approach above.
Full example
A complete working script (Safe v0.3.0 on Arbitrum Sepolia, with sponsored gas and a mint transaction) is available on GitHub: onchain-identifier.ts.
Marker layout
What's the format of the identifier?
The identifier is 32 bytes and follows the format below:
5afe 00 6363643438383836663461336661366162653539 646561 393238 653366
Check the last 32 bytes of the callData field in a UserOperationEvent log, or inside the handleOps tx input, to see how the identifier appears after the transaction is executed.
5afe │ 00 │ project(20) │ platform(3) │ tool(3) │ toolVersion(3)
└─prefix │
version
Each variable-content field is keccak256(value) truncated to its byte width.
Prefix hash
- Type: 2 bytes
- Example:
5afe
Static prefix to identify the Safe on-chain identifier.
Version hash
- Type: 1 byte
- Example:
00
Version number of the Safe on-chain identifier format.
Project hash
- Type: 20 bytes
- Example:
6363643438383836663461336661366162653539
Truncated hash of the project's name (for example, "Gnosis", "CoW Swap").
Platform hash
- Type: 3 bytes
- Example:
646561
Truncated hash of the platform's name (for example, "Web", "Mobile", "Safe App", "Widget").
Tool hash
- Type: 3 bytes
- Example:
393238
Truncated hash of the tool's name (for example, "protocol-kit", "relay-kit", or any custom tool built by projects).
Tool version hash
- Type: 3 bytes
- Example:
653366
Truncated hash of the tool's version (for example, "1.0.0", "1.0.1").
Submission Form
The Safe team aims to better understand and recognise key contributors who are driving the adoption of smart accounts within the ecosystem. By submitting your on-chain identifiers through the provided form, you will help Safe accurately attribute activity.
You can fill out the form by clicking this link.