Agent Commerce Protocol (ACP)
View Sourceraxol_acp is an Elixir/OTP implementation of the Virtuals ACP for selling agent services on Base. Where raxol_payments is about paying (an agent that buys things), raxol_acp is about being paid (an agent that offers a service and accepts on-chain settlement).
Status: pre-alpha. Not yet on Hex; use the path dep at packages/raxol_acp/.
Job Lifecycle
Every job is a state machine. One supervised Raxol.ACP.JobSession runs per active job, registered by {chain_id, job_id}:
:open -> :budget_set -> :funded -> :submitted -> :completed
\-> :rejected
(any non-terminal) -> :expiredRaxol.ACP.JobSession.Status is a pure module holding the status enum and the legal transition graph. JobSession is the GenServer: it tracks role-aware status, keeps a chronological entry log, notifies subscribers, and emits [:raxol, :acp, :job_session, :transition] telemetry on every change. apply_event/3 applies an OBSERVED status (an on-chain or SSE event) directly, bypassing role and adjacency gating.
{:ok, _pid} =
Raxol.ACP.JobSession.Supervisor.start_session(
chain_id: 8453,
job_id: "job-42",
role: :provider
)
# Provider actions transition the session; the Provider driver (below)
# pairs each with the matching hook write on-chain.
{:ok, :budget_set} = Raxol.ACP.JobSession.set_budget({8453, "job-42"}, budget)
{:ok, :submitted} = Raxol.ACP.JobSession.submit({8453, "job-42"}, deliverable)The seller-side glue is Raxol.ACP.JobSession.Provider: for each lifecycle step it invokes the offering Handler (via JobSession.HandlerSeam), writes the hook call on-chain through HookClient + ProviderAdapter (the commit point -- a failed write leaves the session untouched), then mirrors the resulting status with apply_event/3.
Offerings
An offering is a service the agent sells. Declared via the Offering DSL:
defmodule Raxol.ACP.Offerings.SentimentAnalysis do
use Raxol.ACP.Offering,
name: "Sentiment Analysis",
price_usdc: 10,
sla_minutes: 5,
cluster: "analytics"
# Decide whether to take the job. Return {:accept, response} to accept
# and set the budget, or {:reject, reason} to bow out.
@impl true
def handle_request(request, _ctx) do
{:accept, %{quoted_usdc: 10, text: request.text}}
end
# Payment is escrowed; produce the deliverable.
@impl true
def handle_deliver(request, _ctx) do
{:deliver, %{sentiment: analyze(request.text)}}
end
endThe DSL injects the Handler behaviour and registers metadata in the ETS-backed Registry. JobSession.Provider invokes the handler at each lifecycle step and writes the matching hook call on-chain with the configured wallet.
Xochi Cross-Chain Transfer (the first offering)
Raxol.ACP.Xochi.TransferOffering is the first offering: a pure storefront for cross-chain stablecoin transfers. raxol never signs or holds the buyer's funds.
- The buyer quotes and signs a Xochi intent themselves (
Raxol.Payments.Protocols.Xochi.quote_and_sign/3) and puts the signed bundle in the job requirement'ssigned_intent. - On delivery,
Raxol.ACP.Xochi.Settlerrelays that bundle to Xochi viaexecute_signed/2(no re-signing) and polls it to settlement; the deliverable is the on-chain settlement tx hashes. - The transfer settles through Xochi off-escrow, so the ACP core's take never bites it. The job is a plain job (
hook = address(0)); the ACP budget is only raxol's storefront fee -- 8 bps of the transfer, set via:fee_bps. On completion the provider netsbudget * 0.90.
packages/raxol_acp/examples/buyer_signed_intent.exs shows the buyer flow and the requirement/deliverable schemas to register on the marketplace.
On-Chain Writes
The v2 model writes hook calls to the active AgenticCommerceV3 core; there is no separate memo model (the v1 createMemo / Raxol.ACP.ContractClient write surface and the :acp_version switch were retired -- see MIGRATION_V2.md). Raxol.ACP.HookClient exposes set_budget / submit / complete / reject, each dispatched through an injected Raxol.ACP.ProviderAdapter:
SCA: sponsored ERC-4337 v0.7 UserOps viaRaxol.ACP.Wallet.SCA(Alchemy Modular Account v2 + paymaster). Self-deploys the account on the first write.JSONRPC: a plain EOA signing EIP-1559 typed transactions (Raxol.ACP.Onchain.{RPC, Transaction, RLP}), with nonce assignment serialized throughRaxol.ACP.Wallet.NonceServer.Mock: in-process, for tests andmix raxol_acp.bench.
Raxol.ACP.ABI hand-rolls the Solidity encoder for the ACP methods. Real ABIs are vendored under priv/abi/; verified Base addresses live in Raxol.ACP.Chain (the active acp_core_address plus the hook/router/subscription addresses; the legacy acp_contract_address/acp_router_address remain only for indexer back-compat).
Expiry
JobSession reaches the terminal :expired status via expire/2 from any non-terminal status, so a job whose counterparty abandoned it can be closed rather than left wedged. :expired, :completed, and :rejected are terminal -- the session process stops with :normal once it reaches one. On-chain escrow handling on expiry is the AgenticCommerceV3 core's responsibility.
Nonce Serialization
The Raxol.ACP.Wallet.NonceServer GenServer serializes EVM nonce assignment through its mailbox, so two concurrent EOA writes from one wallet can never sign the same nonce. ProviderAdapter.JSONRPC routes every send through it. A send that fails before broadcast resyncs the counter, so the next send re-fetches the pending nonce from chain and re-fills the gap rather than leaving a hole that would strand every later transaction. The SCA/UserOp path uses ERC-4337 EntryPoint nonces and is unaffected.
Seller Stack
Opt-in via :seller_enabled in config:
Backend.InMemory: in-process request queue (default)Queue: bounded mailbox, backpressureRuntime: worker pool dispatching to handlersSupervisor: ties it all together
Backend.WebSocket (Socket.IO v4 / Engine.IO over Mint.WebSocket, talking to Virtuals' relayer) is implemented alongside Backend.InMemory; the Queue drives JobSession.Provider. The protocol spec is available via the virtuals-protocol-acp skill.
Status
Pre-alpha, not yet on Hex. The Wallet.SCA ERC-4337 stack, the real vendored ABIs (priv/abi/), and the Backend.WebSocket Socket.IO client are implemented; the on-chain paths are fork-validated against the real deployed core on Base. mix raxol_acp.bench runs end-to-end against the InMemory backend.
See Also
- Agentic Commerce: the buyer side (raxol_payments)
- Agent Framework: the runtime hosting the seller