feat(x402): apply the paid unlock to agent offers by type, not a configured path - #819
Draft
bussyjd wants to merge 1 commit into
Draft
feat(x402): apply the paid unlock to agent offers by type, not a configured path#819bussyjd wants to merge 1 commit into
bussyjd wants to merge 1 commit into
Conversation
…igured path
The paid unlock gate selected its offer by comparing rule.StripPrefix to a
global authCaptureUnlock.offerPrefix string. That made the platform fee
something an operator had to opt into, by hand, for exactly one offer per
stack — and a node operator has no incentive to opt into a fee that goes to
someone else. It has to ship with the product.
isUnlockOffer now keys on the offer TYPE: every agent offer is unlock-gated,
http offers never are. rule.AgentRuntime is already populated from the
ServiceOffer in serviceoffer_source.go and is the same signal
mergeAgentExtras uses to decide a rule is an agent, so this adds no plumbing.
The split matches how the two types are actually sold. An agent is a
conversation a human opens in the chat widget: connect wallet, pay once to
mint the session, then per-turn billing with no further fee. An http offer is
a machine-to-machine API with no session concept, and gating it would tax
per-request traffic that never signs in. In practice a stack hosts a small
number of agent offers alongside many http APIs, so few offers become gated
and the rest are untouched.
Consequences:
- offerPrefix is removed. There is no longer a one-unlock-offer-per-stack
ceiling, and no per-offer opt-in.
- Price, PayTo and Network become OVERRIDES that fall back to the agent
offer's own values. This is load-bearing once several agents are covered:
without it every agent's unlock revenue would land in one wallet at one
price. The fallbacks are applied BEFORE Validate so it checks the values
that will actually be advertised, and an unlock priced by neither config
nor offer is now rejected rather than advertised at zero.
- handleAuthEndpoints moves its existing authRuleForPrefix lookup above the
check so it can test the rule instead of the path. Same call, earlier.
Also sidesteps the buyer-compatibility problem that a fee-on-everything
design would have: auth-capture only ever appears on the agent unlock, which
is a browser widget we control, so no third-party exact-only buyer meets it.
Tests pin the rule in both directions (agent gated, http not; a second agent
also gated), keep the operator kill switch honest, and cover the nil cases.
Verified the new test fails when the selector is reverted to a prefix match.
bussyjd
force-pushed
the
feat/authcapture-agent-default
branch
from
August 11, 2026 14:10
c0e5993 to
a7afe62
Compare
bussyjd
marked this pull request as draft
August 11, 2026 14:45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the platform fee ship with the product instead of being an operator opt-in.
Problem
The paid unlock gate picked its offer by string-matching
rule.StripPrefixagainst a globalauthCaptureUnlock.offerPrefix. Two consequences, both bad for a platform fee:Change
isUnlockOffernow keys on the offer type:Every agent offer is unlock-gated; http offers never are.
rule.AgentRuntimeis already populated from the ServiceOffer (serviceoffer_source.go:306-308) and is the same signalmergeAgentExtrasuses to decide a rule is an agent — no new plumbing.Why type is the right axis
It matches how the two types are sold.
An agent is a conversation a human opens in the chat widget: connect wallet → pay once → session minted → per-turn billing, no further fee. An http offer is a machine-to-machine API with no session concept; gating it would tax per-request traffic that never signs in.
A stack typically hosts one or a few agent offers alongside many
type: httpAPIs, so in practice a small number of offers become unlock-gated and the rest are untouched:agenthttpMulti-agent correctness
Price,PayToandNetworkbecome overrides, falling back to the agent offer's own values.This is load-bearing the moment more than one agent is covered: without it every agent's unlock revenue would land in one wallet at one price. The fallbacks are applied before
Validate()so it checks the values that will actually be advertised, and an unlock priced by neither config nor offer is now rejected rather than advertised at zero.offerPrefixis removed.Also
handleAuthEndpointsmoves its existingauthRuleForPrefixlookup above the check so it can test the rule rather than the path — same call, twelve lines earlier.This also sidesteps the buyer-compatibility problem a fee-on-everything design would hit:
auth-captureonly ever appears on the agent unlock, which is a browser widget we control, so no third-partyexact-only buyer encounters it.Tests
unlock_agent_default_test.gopins the rule in both directions (agent gated, http not, a second agent also gated), keeps the operator kill switch honest, and covers the nil cases. Verified it fails when the selector is reverted to a prefix match:go build ./...,go vet, and thex402,serviceoffercontrollerandembedsuites all pass.Sequencing — needs a follow-up on #818
#818 templates
authCaptureUnlockinto the Helm chart includingofferPrefix, which this PR removes, and defaultsenabled: false. If both land as-is the chart would render a dead key and the fee would stay off.Whichever merges second should drop
offerPrefixfrombase/values.yaml, the helmfile state values and thex402-pricing.mdreference, and flip the default toenabled: truewith the platformfeeRecipientand bps — which is the whole point of type-based selection.This PR deliberately does not touch those files, to keep the verifier change reviewable on its own.