Description
Best practices for parsing on-chain metadata from MetadataSet events and merging with contract state and off-chain metadata.
Audience: Backend developers, indexer authors, explorer builders
Overview
ERC-8004 supports on-chain metadata stored as key-value pairs via the setMetadata(uint256 agentId, string key, bytes value) function. This guide covers how to properly parse, decode, and merge this data.
Empty Value Handling
Problem
When parsing on-chain metadata from MetadataSet events, empty hex values (0x, 0x0) are ambiguous:
- Are they valid values (empty string)?
- Or do they indicate "not set" (absence of data)?
Solution
Implementations SHOULD treat empty hex values as "not set" rather than valid values.
Rationale: Empty hex values indicate the absence of data, not a valid configuration. Treating them as empty strings can cause false conflicts.
Implementation
ALGORITHM: DecodeHexValue(hexValue)
INPUT: hexValue - bytes value from MetadataSet event
OUTPUT: decoded value or NULL
BEGIN
// Handle empty values
IF hexValue = "0x" OR hexValue = "0x0" THEN
RETURN NULL // Treat as "not set"
END IF
// Decode based on type (address, string, uint256, etc.)
// ... type-specific decoding logic ...
END2
3
4
5
6
7
8
9
10
11
12
13
Example (Python):
def decode_hex_value(hex_value: str) -> Any:
if not hex_value:
return None
# Handle empty value - return None to indicate "not set"
if hex_value == "0x" or hex_value == "0x0":
return None # ✅ Correct
# NOT: return "" # ❌ Wrong
# ... rest of decoding logic ...2
3
4
5
6
7
8
9
10
Metadata Merging
Three-Tier Precedence Order
When multiple sources provide the same field, implementations SHOULD use this priority order:
Contract State (highest priority)
- Direct contract getter functions (e.g.,
agentWallet()) - Authoritative current value set by owner
- Direct contract getter functions (e.g.,
On-chain Metadata (medium priority)
MetadataSetevent key-value pairs- Indexed fields, may be outdated
Off-chain Metadata (lowest priority)
- AgentURI JSON content
- Most flexible, least authoritative
Merging Algorithm
ALGORITHM: MergeMetadata(onchain, offchain, hardcoded)
INPUT:
- onchain: decoded on-chain metadata (from MetadataSet events)
- offchain: off-chain metadata (from AgentURI)
- hardcoded: contract state (from getters)
OUTPUT: merged metadata with conflicts list
BEGIN
merged ← empty dict
conflicts ← empty list
// Step 1: Add off-chain data (lowest priority)
FOR EACH key, value IN offchain DO
IF value is NULL OR value = "" THEN
CONTINUE // Skip empty values
END IF
merged[key] ← value
END FOR
// Step 2: Override with on-chain data (higher priority)
FOR EACH key, value IN onchain DO
IF value is NULL OR value = "" THEN
CONTINUE // Skip empty values
END IF
IF key IN merged AND merged[key] ≠ value THEN
conflicts.append({
type: "onchain_offchain_conflict",
field: key,
onchain_value: value,
offchain_value: merged[key]
})
END IF
merged[key] ← value
END FOR
// Step 3: Override with hardcoded fields (highest priority)
FOR EACH key, value IN hardcoded DO
IF value is NULL OR value = "" THEN
CONTINUE // Skip empty values
END IF
IF key IN merged AND merged[key] ≠ value THEN
conflicts.append({
type: "hardcoded_override",
field: key,
hardcoded_value: value,
overridden_value: merged[key]
})
END IF
merged[key] ← value
END FOR
RETURN (merged, conflicts)
END2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Conflict Detection
Valid Conflicts
Conflicts SHOULD only be reported when non-empty values differ:
✅ Valid Conflict:
Contract State: agent_wallet = 0xaaaa (non-empty)
Onchain Metadata: agent_wallet = 0xbbbb (non-empty, different)
→ Report: WA081 "Contract state conflicts with on-chain metadata"
✅ Valid Conflict:
Onchain Metadata: agent_wallet = 0xaaaa (non-empty)
Offchain Metadata: agent_wallet = 0xbbbb (non-empty, different)
→ Report: WA080 "Conflict between on-chain and off-chain metadata"2
3
4
5
6
7
8
9
Not Conflicts
Empty values SHOULD NOT trigger conflicts:
❌ Not a Conflict:
Contract State: agent_wallet = 0xd5d6... (non-empty)
Onchain Metadata: agent_wallet = 0x (empty → NULL)
→ No conflict (empty value is ignored)
❌ Not a Conflict:
Onchain Metadata: agent_wallet = 0xaaaa (non-empty)
Offchain Metadata: (field not present)
→ No conflict (missing field is not a conflict)2
3
4
5
6
7
8
9
Error Codes
8004scan Error Codes
8004scan-specific: The following error codes are used by 8004scan's validation system. Other implementations MAY use different codes.
WA080 - Warning
- Message: "Conflict between on-chain and off-chain metadata"
- Severity: Warning (parse succeeds)
- Fields:
onchain_value,offchain_value
WA081 - Warning
- Message: "Contract state conflicts with on-chain metadata"
- Severity: Warning (parse succeeds)
- Fields:
contract_value,onchain_metadata_value
IA050 - Info
- Message: "Field value from contract state (on-chain metadata empty or not set)"
- Severity: Info (informational only)
- Fields: Varies by field (e.g.,
agent_wallet) - Reason: On-chain metadata is empty (
0x) or not set, contract state value used
Severity Guidelines
| Level | Description | Action |
|---|---|---|
| Error | Critical issues preventing parsing | Parse fails |
| Warning | Functional issues, data inconsistency | Parse succeeds |
| Info | Recommendations, best practices | Advisory only |
Rationale: Metadata conflicts are warnings, not errors. The agent can still be indexed using the higher-priority value.
Error Reporting
Clear Source Identification
Error messages SHOULD clearly identify which sources conflict:
Good Messages ✅:
"Contract state conflicts with on-chain metadata: agentWallet"
"Conflict between on-chain and off-chain metadata: agentWallet"2
Ambiguous Messages ❌:
"Conflict between on-chain and off-chain: agentWallet"
// When contract state is involved, this is misleading2
Error Structure
{
"code": "WA081",
"field": "agent_wallet",
"message": "Contract state conflicts with on-chain metadata",
"contract_value": "0xd5d6d96fa23455ec5e3c00633f85f364d3f5a291",
"onchain_metadata_value": "0x1234567890abcdef1234567890abcdef12345678"
}2
3
4
5
6
7
Real-World Example
Scenario: Agent with Empty On-chain Metadata
Data Sources:
Contract State (via getter): agentWallet = 0xd5d6d96fa23455ec5e3c00633f85f364d3f5a291
On-chain Metadata (MetadataSet): agentWallet = 0x (empty)
Off-chain Metadata (AgentURI): (no agentWallet field)
Processing:
1. decode_hex_value("0x") → NULL (empty value)
2. merge_metadata():
- Step 1: Offchain - no agentWallet field
- Step 2: Onchain - agentWallet = NULL, skipped (empty value)
- Step 3: Hardcoded - agentWallet = 0xd5d6..., added
Final Result:
merged.agent_wallet = 0xd5d6d96fa23455ec5e3c00633f85f364d3f5a291
conflicts = [] (no conflicts)
parse_status.status = "success"2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Before Fix ❌:
{
"status": "error",
"errors": [
{
"type": "metadata_conflict",
"field": "agent_wallet",
"message": "Conflict between on-chain and off-chain: agent_wallet",
"onchain_value": "0xd5d6...",
"offchain_value": ""
}
]
}2
3
4
5
6
7
8
9
10
11
12
After Fix ✅:
{
"status": "success",
"errors": [],
"warnings": [],
"info": [
{
"code": "IA050",
"field": "agent_wallet",
"message": "Field value from contract state (on-chain metadata empty or not set)",
"source": "contract_state"
}
],
"agent_wallet": "0xd5d6d96fa23455ec5e3c00633f85f364d3f5a291"
}2
3
4
5
6
7
8
9
10
11
12
13
14
Common Pitfalls
Pitfall 1: Treating Empty Strings as Valid Values
Problem:
if hex_value == "0x":
return "" # ❌ Empty string participates in merging2
Solution:
if hex_value == "0x":
return None # ✅ NULL is filtered out2
Pitfall 2: Not Filtering Empty Values in Merge
Problem:
for key, value in onchain.items():
if value is None: # ❌ Only filters None
continue2
3
Solution:
for key, value in onchain.items():
if value is None or value == "": # ✅ Also filters empty strings
continue2
3
Pitfall 3: Ambiguous Error Messages
Problem:
# When hardcoded value conflicts with on-chain metadata
message = "Conflict between on-chain and off-chain" # ❌ Misleading2
Solution:
if conflict_type == "hardcoded_override":
message = "Contract state conflicts with on-chain metadata" # ✅ Clear
else:
message = "Conflict between on-chain and off-chain metadata"2
3
4
Testing Checklist
Implementations SHOULD test these scenarios:
- ✅ Empty hex values (
0x,0x0) return NULL - ✅ NULL values are filtered out in merge
- ✅ Empty strings are filtered out in merge
- ✅ No conflict when on-chain value is empty
- ✅ Conflict reported when non-empty values differ
- ✅ Contract state has highest priority
- ✅ Error codes correctly distinguish conflict types
- ✅ Error messages are clear and accurate
Resources
- Agent Metadata Standard - Full schema specification
- Agent Metadata Parsing - Parser architecture and validation
- Error Codes - Complete error code reference (if available)
- EIP-8004 Specification - Official specification