fix: aggregate ERC-8004 registration services - #363

Merged
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean
Apr 22, 2026
Merged

fix: aggregate ERC-8004 registration services#363
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • aggregate ERC-8004 registration across registration-enabled ServiceOffers into one shared agent document
  • fan the shared registration status back out to all participating offers instead of marking non-owners as SingletonConflict
  • hand the singleton registration request off cleanly when the current owner offer disappears, and refresh the shared document whenever offer state changes
  • add controller/render tests covering aggregated services, shared registration status, and the final published registration payload

Context

Fixes#361.

ERC-8004 expects a single agentURI / /.well-known/agent-registration.json document with a services array. The previous controller behavior treated the path as singleton but still rendered the document from exactly one offer, which broke multi-service agents and demo flows.

Before / After

Old behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B{"Which offer owns /.well-known/agent-registration.json?"}
B -->|Oldest offer| C["Publish registration doc from one ServiceOffer only"]
B -->|All later offers| D["Registered=False\nSingletonConflict"]
C --> E["Only owner gets Registered=True"]
E --> F["Only owner can contribute to Ready-dependent discovery surfaces"]
Loading

New behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B["Select canonical registration owner"]
B --> C["Build one shared ERC-8004 document"]
C --> D["Append all eligible offer endpoints into services[]"]
D --> E["Publish shared /.well-known/agent-registration.json"]
E --> F["Propagate shared agentId / txHash / registration status to all participating offers"]
Loading

Owner Handoff

sequenceDiagram
participant OfferA as "Owner Offer"
participant Ctrl as "serviceoffer-controller"
participant Req as "Shared RegistrationRequest"
participant OfferB as "Next Eligible Offer"
OfferA->>Ctrl: deleted or disabled
Ctrl->>Req: stop using old owner identity
Ctrl->>OfferB: enqueue reconcile
Ctrl->>Req: recreate or refresh under new owner
Req->>Ctrl: shared status updated
Ctrl->>OfferB: set Registered=True via shared agent doc
Loading

Concrete Examples

Before

Two offers:

  • demo/review-hello
  • demo/review-blocks

Observed result:

  • review-hello gets Registered=True
  • review-blocks gets Registered=False with SingletonConflict
  • shared registration document contains only the review-hello endpoint

Representative status on the later offer:

conditions:
- type: Registeredstatus: "False"reason: SingletonConflictmessage: Registration path /.well-known/agent-registration.json is reserved by demo/review-hello

Representative registration document before this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] }
]
}

After

With the same two offers active, both participate in one shared document.

Representative registration document after this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] },
{ "name": "web", "endpoint": "https://example.com/services/review-blocks" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "blocks"] }
]
}

Representative non-owner offer status after this fix:

conditions:
- type: Registeredstatus: "True"reason: Registeredmessage: Shared registration via demo/review-hello recorded agent 42

Why this matches ERC-8004

The fix aligns the controller with the EIP's intended model:

  • one agentURI
  • one agent registration document
  • many services inside services[]
  • optional domain verification via /.well-known/agent-registration.json

Validation

  • go test -v ./internal/serviceoffercontroller
  • go test -run TestNope ./cmd/obol ./internal/erc8004 ./internal/monetizeapi ./internal/x402 ./internal/stack
  • TestBuildRegistrationConfigMap_PublishesAggregatedAgentRegistration verifies that the published agent-registration.json payload contains every expected service endpoint, not just the aggregated in-memory document

@bussyjd
bussyjd requested a review from OisinKyneApril 21, 2026 17:25
@bussyjd
bussyjd marked this pull request as ready for review April 21, 2026 17:25
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from b827753 to 49d340bCompareApril 21, 2026 17:28
@bussyjdbussyjd closed this Apr 21, 2026
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from 49d340b to 899c605CompareApril 21, 2026 17:31
@bussyjd
bussyjd merged commit 0f7d553 into mainApr 22, 2026
6 checks passed
@bussyjd
bussyjd requested a review from OisinKyneApril 22, 2026 18:23
@OisinKyne
OisinKyne deleted the fix/erc8004-shared-registration-clean branch April 23, 2026 19:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate ERC-8004 registration across ServiceOffers

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: aggregate ERC-8004 registration services - #363

Merged
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean
Apr 22, 2026
Merged

fix: aggregate ERC-8004 registration services#363
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • aggregate ERC-8004 registration across registration-enabled ServiceOffers into one shared agent document
  • fan the shared registration status back out to all participating offers instead of marking non-owners as SingletonConflict
  • hand the singleton registration request off cleanly when the current owner offer disappears, and refresh the shared document whenever offer state changes
  • add controller/render tests covering aggregated services, shared registration status, and the final published registration payload

Context

Fixes#361.

ERC-8004 expects a single agentURI / /.well-known/agent-registration.json document with a services array. The previous controller behavior treated the path as singleton but still rendered the document from exactly one offer, which broke multi-service agents and demo flows.

Before / After

Old behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B{"Which offer owns /.well-known/agent-registration.json?"}
B -->|Oldest offer| C["Publish registration doc from one ServiceOffer only"]
B -->|All later offers| D["Registered=False\nSingletonConflict"]
C --> E["Only owner gets Registered=True"]
E --> F["Only owner can contribute to Ready-dependent discovery surfaces"]
Loading

New behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B["Select canonical registration owner"]
B --> C["Build one shared ERC-8004 document"]
C --> D["Append all eligible offer endpoints into services[]"]
D --> E["Publish shared /.well-known/agent-registration.json"]
E --> F["Propagate shared agentId / txHash / registration status to all participating offers"]
Loading

Owner Handoff

sequenceDiagram
participant OfferA as "Owner Offer"
participant Ctrl as "serviceoffer-controller"
participant Req as "Shared RegistrationRequest"
participant OfferB as "Next Eligible Offer"
OfferA->>Ctrl: deleted or disabled
Ctrl->>Req: stop using old owner identity
Ctrl->>OfferB: enqueue reconcile
Ctrl->>Req: recreate or refresh under new owner
Req->>Ctrl: shared status updated
Ctrl->>OfferB: set Registered=True via shared agent doc
Loading

Concrete Examples

Before

Two offers:

  • demo/review-hello
  • demo/review-blocks

Observed result:

  • review-hello gets Registered=True
  • review-blocks gets Registered=False with SingletonConflict
  • shared registration document contains only the review-hello endpoint

Representative status on the later offer:

conditions:
- type: Registeredstatus: "False"reason: SingletonConflictmessage: Registration path /.well-known/agent-registration.json is reserved by demo/review-hello

Representative registration document before this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] }
]
}

After

With the same two offers active, both participate in one shared document.

Representative registration document after this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] },
{ "name": "web", "endpoint": "https://example.com/services/review-blocks" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "blocks"] }
]
}

Representative non-owner offer status after this fix:

conditions:
- type: Registeredstatus: "True"reason: Registeredmessage: Shared registration via demo/review-hello recorded agent 42

Why this matches ERC-8004

The fix aligns the controller with the EIP's intended model:

  • one agentURI
  • one agent registration document
  • many services inside services[]
  • optional domain verification via /.well-known/agent-registration.json

Validation

  • go test -v ./internal/serviceoffercontroller
  • go test -run TestNope ./cmd/obol ./internal/erc8004 ./internal/monetizeapi ./internal/x402 ./internal/stack
  • TestBuildRegistrationConfigMap_PublishesAggregatedAgentRegistration verifies that the published agent-registration.json payload contains every expected service endpoint, not just the aggregated in-memory document

@bussyjd
bussyjd requested a review from OisinKyneApril 21, 2026 17:25
@bussyjd
bussyjd marked this pull request as ready for review April 21, 2026 17:25
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from b827753 to 49d340bCompareApril 21, 2026 17:28
@bussyjdbussyjd closed this Apr 21, 2026
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from 49d340b to 899c605CompareApril 21, 2026 17:31
@bussyjd
bussyjd merged commit 0f7d553 into mainApr 22, 2026
6 checks passed
@bussyjd
bussyjd requested a review from OisinKyneApril 22, 2026 18:23
@OisinKyne
OisinKyne deleted the fix/erc8004-shared-registration-clean branch April 23, 2026 19:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate ERC-8004 registration across ServiceOffers

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: aggregate ERC-8004 registration services - #363

Merged
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean
Apr 22, 2026
Merged

fix: aggregate ERC-8004 registration services#363
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • aggregate ERC-8004 registration across registration-enabled ServiceOffers into one shared agent document
  • fan the shared registration status back out to all participating offers instead of marking non-owners as SingletonConflict
  • hand the singleton registration request off cleanly when the current owner offer disappears, and refresh the shared document whenever offer state changes
  • add controller/render tests covering aggregated services, shared registration status, and the final published registration payload

Context

Fixes#361.

ERC-8004 expects a single agentURI / /.well-known/agent-registration.json document with a services array. The previous controller behavior treated the path as singleton but still rendered the document from exactly one offer, which broke multi-service agents and demo flows.

Before / After

Old behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B{"Which offer owns /.well-known/agent-registration.json?"}
B -->|Oldest offer| C["Publish registration doc from one ServiceOffer only"]
B -->|All later offers| D["Registered=False\nSingletonConflict"]
C --> E["Only owner gets Registered=True"]
E --> F["Only owner can contribute to Ready-dependent discovery surfaces"]
Loading

New behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B["Select canonical registration owner"]
B --> C["Build one shared ERC-8004 document"]
C --> D["Append all eligible offer endpoints into services[]"]
D --> E["Publish shared /.well-known/agent-registration.json"]
E --> F["Propagate shared agentId / txHash / registration status to all participating offers"]
Loading

Owner Handoff

sequenceDiagram
participant OfferA as "Owner Offer"
participant Ctrl as "serviceoffer-controller"
participant Req as "Shared RegistrationRequest"
participant OfferB as "Next Eligible Offer"
OfferA->>Ctrl: deleted or disabled
Ctrl->>Req: stop using old owner identity
Ctrl->>OfferB: enqueue reconcile
Ctrl->>Req: recreate or refresh under new owner
Req->>Ctrl: shared status updated
Ctrl->>OfferB: set Registered=True via shared agent doc
Loading

Concrete Examples

Before

Two offers:

  • demo/review-hello
  • demo/review-blocks

Observed result:

  • review-hello gets Registered=True
  • review-blocks gets Registered=False with SingletonConflict
  • shared registration document contains only the review-hello endpoint

Representative status on the later offer:

conditions:
- type: Registeredstatus: "False"reason: SingletonConflictmessage: Registration path /.well-known/agent-registration.json is reserved by demo/review-hello

Representative registration document before this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] }
]
}

After

With the same two offers active, both participate in one shared document.

Representative registration document after this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] },
{ "name": "web", "endpoint": "https://example.com/services/review-blocks" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "blocks"] }
]
}

Representative non-owner offer status after this fix:

conditions:
- type: Registeredstatus: "True"reason: Registeredmessage: Shared registration via demo/review-hello recorded agent 42

Why this matches ERC-8004

The fix aligns the controller with the EIP's intended model:

  • one agentURI
  • one agent registration document
  • many services inside services[]
  • optional domain verification via /.well-known/agent-registration.json

Validation

  • go test -v ./internal/serviceoffercontroller
  • go test -run TestNope ./cmd/obol ./internal/erc8004 ./internal/monetizeapi ./internal/x402 ./internal/stack
  • TestBuildRegistrationConfigMap_PublishesAggregatedAgentRegistration verifies that the published agent-registration.json payload contains every expected service endpoint, not just the aggregated in-memory document

@bussyjd
bussyjd requested a review from OisinKyneApril 21, 2026 17:25
@bussyjd
bussyjd marked this pull request as ready for review April 21, 2026 17:25
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from b827753 to 49d340bCompareApril 21, 2026 17:28
@bussyjdbussyjd closed this Apr 21, 2026
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from 49d340b to 899c605CompareApril 21, 2026 17:31
@bussyjd
bussyjd merged commit 0f7d553 into mainApr 22, 2026
6 checks passed
@bussyjd
bussyjd requested a review from OisinKyneApril 22, 2026 18:23
@OisinKyne
OisinKyne deleted the fix/erc8004-shared-registration-clean branch April 23, 2026 19:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate ERC-8004 registration across ServiceOffers

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: aggregate ERC-8004 registration services - #363

Merged
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean
Apr 22, 2026
Merged

fix: aggregate ERC-8004 registration services#363
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • aggregate ERC-8004 registration across registration-enabled ServiceOffers into one shared agent document
  • fan the shared registration status back out to all participating offers instead of marking non-owners as SingletonConflict
  • hand the singleton registration request off cleanly when the current owner offer disappears, and refresh the shared document whenever offer state changes
  • add controller/render tests covering aggregated services, shared registration status, and the final published registration payload

Context

Fixes#361.

ERC-8004 expects a single agentURI / /.well-known/agent-registration.json document with a services array. The previous controller behavior treated the path as singleton but still rendered the document from exactly one offer, which broke multi-service agents and demo flows.

Before / After

Old behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B{"Which offer owns /.well-known/agent-registration.json?"}
B -->|Oldest offer| C["Publish registration doc from one ServiceOffer only"]
B -->|All later offers| D["Registered=False\nSingletonConflict"]
C --> E["Only owner gets Registered=True"]
E --> F["Only owner can contribute to Ready-dependent discovery surfaces"]
Loading

New behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B["Select canonical registration owner"]
B --> C["Build one shared ERC-8004 document"]
C --> D["Append all eligible offer endpoints into services[]"]
D --> E["Publish shared /.well-known/agent-registration.json"]
E --> F["Propagate shared agentId / txHash / registration status to all participating offers"]
Loading

Owner Handoff

sequenceDiagram
participant OfferA as "Owner Offer"
participant Ctrl as "serviceoffer-controller"
participant Req as "Shared RegistrationRequest"
participant OfferB as "Next Eligible Offer"
OfferA->>Ctrl: deleted or disabled
Ctrl->>Req: stop using old owner identity
Ctrl->>OfferB: enqueue reconcile
Ctrl->>Req: recreate or refresh under new owner
Req->>Ctrl: shared status updated
Ctrl->>OfferB: set Registered=True via shared agent doc
Loading

Concrete Examples

Before

Two offers:

  • demo/review-hello
  • demo/review-blocks

Observed result:

  • review-hello gets Registered=True
  • review-blocks gets Registered=False with SingletonConflict
  • shared registration document contains only the review-hello endpoint

Representative status on the later offer:

conditions:
- type: Registeredstatus: "False"reason: SingletonConflictmessage: Registration path /.well-known/agent-registration.json is reserved by demo/review-hello

Representative registration document before this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] }
]
}

After

With the same two offers active, both participate in one shared document.

Representative registration document after this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] },
{ "name": "web", "endpoint": "https://example.com/services/review-blocks" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "blocks"] }
]
}

Representative non-owner offer status after this fix:

conditions:
- type: Registeredstatus: "True"reason: Registeredmessage: Shared registration via demo/review-hello recorded agent 42

Why this matches ERC-8004

The fix aligns the controller with the EIP's intended model:

  • one agentURI
  • one agent registration document
  • many services inside services[]
  • optional domain verification via /.well-known/agent-registration.json

Validation

  • go test -v ./internal/serviceoffercontroller
  • go test -run TestNope ./cmd/obol ./internal/erc8004 ./internal/monetizeapi ./internal/x402 ./internal/stack
  • TestBuildRegistrationConfigMap_PublishesAggregatedAgentRegistration verifies that the published agent-registration.json payload contains every expected service endpoint, not just the aggregated in-memory document

@bussyjd
bussyjd requested a review from OisinKyneApril 21, 2026 17:25
@bussyjd
bussyjd marked this pull request as ready for review April 21, 2026 17:25
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from b827753 to 49d340bCompareApril 21, 2026 17:28
@bussyjdbussyjd closed this Apr 21, 2026
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from 49d340b to 899c605CompareApril 21, 2026 17:31
@bussyjd
bussyjd merged commit 0f7d553 into mainApr 22, 2026
6 checks passed
@bussyjd
bussyjd requested a review from OisinKyneApril 22, 2026 18:23
@OisinKyne
OisinKyne deleted the fix/erc8004-shared-registration-clean branch April 23, 2026 19:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate ERC-8004 registration across ServiceOffers

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: aggregate ERC-8004 registration services - #363

Merged
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean
Apr 22, 2026
Merged

fix: aggregate ERC-8004 registration services#363
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • aggregate ERC-8004 registration across registration-enabled ServiceOffers into one shared agent document
  • fan the shared registration status back out to all participating offers instead of marking non-owners as SingletonConflict
  • hand the singleton registration request off cleanly when the current owner offer disappears, and refresh the shared document whenever offer state changes
  • add controller/render tests covering aggregated services, shared registration status, and the final published registration payload

Context

Fixes#361.

ERC-8004 expects a single agentURI / /.well-known/agent-registration.json document with a services array. The previous controller behavior treated the path as singleton but still rendered the document from exactly one offer, which broke multi-service agents and demo flows.

Before / After

Old behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B{"Which offer owns /.well-known/agent-registration.json?"}
B -->|Oldest offer| C["Publish registration doc from one ServiceOffer only"]
B -->|All later offers| D["Registered=False\nSingletonConflict"]
C --> E["Only owner gets Registered=True"]
E --> F["Only owner can contribute to Ready-dependent discovery surfaces"]
Loading

New behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B["Select canonical registration owner"]
B --> C["Build one shared ERC-8004 document"]
C --> D["Append all eligible offer endpoints into services[]"]
D --> E["Publish shared /.well-known/agent-registration.json"]
E --> F["Propagate shared agentId / txHash / registration status to all participating offers"]
Loading

Owner Handoff

sequenceDiagram
participant OfferA as "Owner Offer"
participant Ctrl as "serviceoffer-controller"
participant Req as "Shared RegistrationRequest"
participant OfferB as "Next Eligible Offer"
OfferA->>Ctrl: deleted or disabled
Ctrl->>Req: stop using old owner identity
Ctrl->>OfferB: enqueue reconcile
Ctrl->>Req: recreate or refresh under new owner
Req->>Ctrl: shared status updated
Ctrl->>OfferB: set Registered=True via shared agent doc
Loading

Concrete Examples

Before

Two offers:

  • demo/review-hello
  • demo/review-blocks

Observed result:

  • review-hello gets Registered=True
  • review-blocks gets Registered=False with SingletonConflict
  • shared registration document contains only the review-hello endpoint

Representative status on the later offer:

conditions:
- type: Registeredstatus: "False"reason: SingletonConflictmessage: Registration path /.well-known/agent-registration.json is reserved by demo/review-hello

Representative registration document before this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] }
]
}

After

With the same two offers active, both participate in one shared document.

Representative registration document after this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] },
{ "name": "web", "endpoint": "https://example.com/services/review-blocks" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "blocks"] }
]
}

Representative non-owner offer status after this fix:

conditions:
- type: Registeredstatus: "True"reason: Registeredmessage: Shared registration via demo/review-hello recorded agent 42

Why this matches ERC-8004

The fix aligns the controller with the EIP's intended model:

  • one agentURI
  • one agent registration document
  • many services inside services[]
  • optional domain verification via /.well-known/agent-registration.json

Validation

  • go test -v ./internal/serviceoffercontroller
  • go test -run TestNope ./cmd/obol ./internal/erc8004 ./internal/monetizeapi ./internal/x402 ./internal/stack
  • TestBuildRegistrationConfigMap_PublishesAggregatedAgentRegistration verifies that the published agent-registration.json payload contains every expected service endpoint, not just the aggregated in-memory document

@bussyjd
bussyjd requested a review from OisinKyneApril 21, 2026 17:25
@bussyjd
bussyjd marked this pull request as ready for review April 21, 2026 17:25
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from b827753 to 49d340bCompareApril 21, 2026 17:28
@bussyjdbussyjd closed this Apr 21, 2026
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from 49d340b to 899c605CompareApril 21, 2026 17:31
@bussyjd
bussyjd merged commit 0f7d553 into mainApr 22, 2026
6 checks passed
@bussyjd
bussyjd requested a review from OisinKyneApril 22, 2026 18:23
@OisinKyne
OisinKyne deleted the fix/erc8004-shared-registration-clean branch April 23, 2026 19:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate ERC-8004 registration across ServiceOffers

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: aggregate ERC-8004 registration services - #363

Merged
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean
Apr 22, 2026
Merged

fix: aggregate ERC-8004 registration services#363
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • aggregate ERC-8004 registration across registration-enabled ServiceOffers into one shared agent document
  • fan the shared registration status back out to all participating offers instead of marking non-owners as SingletonConflict
  • hand the singleton registration request off cleanly when the current owner offer disappears, and refresh the shared document whenever offer state changes
  • add controller/render tests covering aggregated services, shared registration status, and the final published registration payload

Context

Fixes#361.

ERC-8004 expects a single agentURI / /.well-known/agent-registration.json document with a services array. The previous controller behavior treated the path as singleton but still rendered the document from exactly one offer, which broke multi-service agents and demo flows.

Before / After

Old behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B{"Which offer owns /.well-known/agent-registration.json?"}
B -->|Oldest offer| C["Publish registration doc from one ServiceOffer only"]
B -->|All later offers| D["Registered=False\nSingletonConflict"]
C --> E["Only owner gets Registered=True"]
E --> F["Only owner can contribute to Ready-dependent discovery surfaces"]
Loading

New behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B["Select canonical registration owner"]
B --> C["Build one shared ERC-8004 document"]
C --> D["Append all eligible offer endpoints into services[]"]
D --> E["Publish shared /.well-known/agent-registration.json"]
E --> F["Propagate shared agentId / txHash / registration status to all participating offers"]
Loading

Owner Handoff

sequenceDiagram
participant OfferA as "Owner Offer"
participant Ctrl as "serviceoffer-controller"
participant Req as "Shared RegistrationRequest"
participant OfferB as "Next Eligible Offer"
OfferA->>Ctrl: deleted or disabled
Ctrl->>Req: stop using old owner identity
Ctrl->>OfferB: enqueue reconcile
Ctrl->>Req: recreate or refresh under new owner
Req->>Ctrl: shared status updated
Ctrl->>OfferB: set Registered=True via shared agent doc
Loading

Concrete Examples

Before

Two offers:

  • demo/review-hello
  • demo/review-blocks

Observed result:

  • review-hello gets Registered=True
  • review-blocks gets Registered=False with SingletonConflict
  • shared registration document contains only the review-hello endpoint

Representative status on the later offer:

conditions:
- type: Registeredstatus: "False"reason: SingletonConflictmessage: Registration path /.well-known/agent-registration.json is reserved by demo/review-hello

Representative registration document before this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] }
]
}

After

With the same two offers active, both participate in one shared document.

Representative registration document after this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] },
{ "name": "web", "endpoint": "https://example.com/services/review-blocks" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "blocks"] }
]
}

Representative non-owner offer status after this fix:

conditions:
- type: Registeredstatus: "True"reason: Registeredmessage: Shared registration via demo/review-hello recorded agent 42

Why this matches ERC-8004

The fix aligns the controller with the EIP's intended model:

  • one agentURI
  • one agent registration document
  • many services inside services[]
  • optional domain verification via /.well-known/agent-registration.json

Validation

  • go test -v ./internal/serviceoffercontroller
  • go test -run TestNope ./cmd/obol ./internal/erc8004 ./internal/monetizeapi ./internal/x402 ./internal/stack
  • TestBuildRegistrationConfigMap_PublishesAggregatedAgentRegistration verifies that the published agent-registration.json payload contains every expected service endpoint, not just the aggregated in-memory document

@bussyjd
bussyjd requested a review from OisinKyneApril 21, 2026 17:25
@bussyjd
bussyjd marked this pull request as ready for review April 21, 2026 17:25
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from b827753 to 49d340bCompareApril 21, 2026 17:28
@bussyjdbussyjd closed this Apr 21, 2026
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from 49d340b to 899c605CompareApril 21, 2026 17:31
@bussyjd
bussyjd merged commit 0f7d553 into mainApr 22, 2026
6 checks passed
@bussyjd
bussyjd requested a review from OisinKyneApril 22, 2026 18:23
@OisinKyne
OisinKyne deleted the fix/erc8004-shared-registration-clean branch April 23, 2026 19:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate ERC-8004 registration across ServiceOffers

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: aggregate ERC-8004 registration services - #363

Merged
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean
Apr 22, 2026
Merged

fix: aggregate ERC-8004 registration services#363
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • aggregate ERC-8004 registration across registration-enabled ServiceOffers into one shared agent document
  • fan the shared registration status back out to all participating offers instead of marking non-owners as SingletonConflict
  • hand the singleton registration request off cleanly when the current owner offer disappears, and refresh the shared document whenever offer state changes
  • add controller/render tests covering aggregated services, shared registration status, and the final published registration payload

Context

Fixes#361.

ERC-8004 expects a single agentURI / /.well-known/agent-registration.json document with a services array. The previous controller behavior treated the path as singleton but still rendered the document from exactly one offer, which broke multi-service agents and demo flows.

Before / After

Old behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B{"Which offer owns /.well-known/agent-registration.json?"}
B -->|Oldest offer| C["Publish registration doc from one ServiceOffer only"]
B -->|All later offers| D["Registered=False\nSingletonConflict"]
C --> E["Only owner gets Registered=True"]
E --> F["Only owner can contribute to Ready-dependent discovery surfaces"]
Loading

New behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B["Select canonical registration owner"]
B --> C["Build one shared ERC-8004 document"]
C --> D["Append all eligible offer endpoints into services[]"]
D --> E["Publish shared /.well-known/agent-registration.json"]
E --> F["Propagate shared agentId / txHash / registration status to all participating offers"]
Loading

Owner Handoff

sequenceDiagram
participant OfferA as "Owner Offer"
participant Ctrl as "serviceoffer-controller"
participant Req as "Shared RegistrationRequest"
participant OfferB as "Next Eligible Offer"
OfferA->>Ctrl: deleted or disabled
Ctrl->>Req: stop using old owner identity
Ctrl->>OfferB: enqueue reconcile
Ctrl->>Req: recreate or refresh under new owner
Req->>Ctrl: shared status updated
Ctrl->>OfferB: set Registered=True via shared agent doc
Loading

Concrete Examples

Before

Two offers:

  • demo/review-hello
  • demo/review-blocks

Observed result:

  • review-hello gets Registered=True
  • review-blocks gets Registered=False with SingletonConflict
  • shared registration document contains only the review-hello endpoint

Representative status on the later offer:

conditions:
- type: Registeredstatus: "False"reason: SingletonConflictmessage: Registration path /.well-known/agent-registration.json is reserved by demo/review-hello

Representative registration document before this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] }
]
}

After

With the same two offers active, both participate in one shared document.

Representative registration document after this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] },
{ "name": "web", "endpoint": "https://example.com/services/review-blocks" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "blocks"] }
]
}

Representative non-owner offer status after this fix:

conditions:
- type: Registeredstatus: "True"reason: Registeredmessage: Shared registration via demo/review-hello recorded agent 42

Why this matches ERC-8004

The fix aligns the controller with the EIP's intended model:

  • one agentURI
  • one agent registration document
  • many services inside services[]
  • optional domain verification via /.well-known/agent-registration.json

Validation

  • go test -v ./internal/serviceoffercontroller
  • go test -run TestNope ./cmd/obol ./internal/erc8004 ./internal/monetizeapi ./internal/x402 ./internal/stack
  • TestBuildRegistrationConfigMap_PublishesAggregatedAgentRegistration verifies that the published agent-registration.json payload contains every expected service endpoint, not just the aggregated in-memory document

@bussyjd
bussyjd requested a review from OisinKyneApril 21, 2026 17:25
@bussyjd
bussyjd marked this pull request as ready for review April 21, 2026 17:25
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from b827753 to 49d340bCompareApril 21, 2026 17:28
@bussyjdbussyjd closed this Apr 21, 2026
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from 49d340b to 899c605CompareApril 21, 2026 17:31
@bussyjd
bussyjd merged commit 0f7d553 into mainApr 22, 2026
6 checks passed
@bussyjd
bussyjd requested a review from OisinKyneApril 22, 2026 18:23
@OisinKyne
OisinKyne deleted the fix/erc8004-shared-registration-clean branch April 23, 2026 19:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate ERC-8004 registration across ServiceOffers

2 participants

@bussyjd@OisinKyne
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: aggregate ERC-8004 registration services - #363

Merged
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean
Apr 22, 2026
Merged

fix: aggregate ERC-8004 registration services#363
bussyjd merged 1 commit into
mainfrom
fix/erc8004-shared-registration-clean

Conversation

@bussyjd

@bussyjdbussyjd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • aggregate ERC-8004 registration across registration-enabled ServiceOffers into one shared agent document
  • fan the shared registration status back out to all participating offers instead of marking non-owners as SingletonConflict
  • hand the singleton registration request off cleanly when the current owner offer disappears, and refresh the shared document whenever offer state changes
  • add controller/render tests covering aggregated services, shared registration status, and the final published registration payload

Context

Fixes#361.

ERC-8004 expects a single agentURI / /.well-known/agent-registration.json document with a services array. The previous controller behavior treated the path as singleton but still rendered the document from exactly one offer, which broke multi-service agents and demo flows.

Before / After

Old behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B{"Which offer owns /.well-known/agent-registration.json?"}
B -->|Oldest offer| C["Publish registration doc from one ServiceOffer only"]
B -->|All later offers| D["Registered=False\nSingletonConflict"]
C --> E["Only owner gets Registered=True"]
E --> F["Only owner can contribute to Ready-dependent discovery surfaces"]
Loading

New behavior

flowchart TD
A["Registration-enabled ServiceOffers"] --> B["Select canonical registration owner"]
B --> C["Build one shared ERC-8004 document"]
C --> D["Append all eligible offer endpoints into services[]"]
D --> E["Publish shared /.well-known/agent-registration.json"]
E --> F["Propagate shared agentId / txHash / registration status to all participating offers"]
Loading

Owner Handoff

sequenceDiagram
participant OfferA as "Owner Offer"
participant Ctrl as "serviceoffer-controller"
participant Req as "Shared RegistrationRequest"
participant OfferB as "Next Eligible Offer"
OfferA->>Ctrl: deleted or disabled
Ctrl->>Req: stop using old owner identity
Ctrl->>OfferB: enqueue reconcile
Ctrl->>Req: recreate or refresh under new owner
Req->>Ctrl: shared status updated
Ctrl->>OfferB: set Registered=True via shared agent doc
Loading

Concrete Examples

Before

Two offers:

  • demo/review-hello
  • demo/review-blocks

Observed result:

  • review-hello gets Registered=True
  • review-blocks gets Registered=False with SingletonConflict
  • shared registration document contains only the review-hello endpoint

Representative status on the later offer:

conditions:
- type: Registeredstatus: "False"reason: SingletonConflictmessage: Registration path /.well-known/agent-registration.json is reserved by demo/review-hello

Representative registration document before this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] }
]
}

After

With the same two offers active, both participate in one shared document.

Representative registration document after this fix:

{
"name": "review-hello",
"services": [
{ "name": "web", "endpoint": "https://example.com/services/review-hello" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "hello"] },
{ "name": "web", "endpoint": "https://example.com/services/review-blocks" },
{ "name": "OASF", "version": "0.8", "skills": ["x402-demo", "blocks"] }
]
}

Representative non-owner offer status after this fix:

conditions:
- type: Registeredstatus: "True"reason: Registeredmessage: Shared registration via demo/review-hello recorded agent 42

Why this matches ERC-8004

The fix aligns the controller with the EIP's intended model:

  • one agentURI
  • one agent registration document
  • many services inside services[]
  • optional domain verification via /.well-known/agent-registration.json

Validation

  • go test -v ./internal/serviceoffercontroller
  • go test -run TestNope ./cmd/obol ./internal/erc8004 ./internal/monetizeapi ./internal/x402 ./internal/stack
  • TestBuildRegistrationConfigMap_PublishesAggregatedAgentRegistration verifies that the published agent-registration.json payload contains every expected service endpoint, not just the aggregated in-memory document

@bussyjd
bussyjd requested a review from OisinKyneApril 21, 2026 17:25
@bussyjd
bussyjd marked this pull request as ready for review April 21, 2026 17:25
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from b827753 to 49d340bCompareApril 21, 2026 17:28
@bussyjdbussyjd closed this Apr 21, 2026
@bussyjd
bussyjdforce-pushed the fix/erc8004-shared-registration-clean branch from 49d340b to 899c605CompareApril 21, 2026 17:31
@bussyjd
bussyjd merged commit 0f7d553 into mainApr 22, 2026
6 checks passed
@bussyjd
bussyjd requested a review from OisinKyneApril 22, 2026 18:23
@OisinKyne
OisinKyne deleted the fix/erc8004-shared-registration-clean branch April 23, 2026 19:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate ERC-8004 registration across ServiceOffers

2 participants

@bussyjd@OisinKyne