Skip to main content
You have a scarce on-chain capability to sell. You implement two functions, deploy the adapter, and the broker pays you amount - fee every time it grants. By the end you will have an adapter the broker can call and, optionally, a discovery listing. Two worked adapters follow: AllocationAdapter (the floor, no external dependency), then TimeboostAdapter (a categorically different scarcity, which is what proves the primitive is general).

The interface

An adapter implements ICapabilityAdapter: two functions.
interface ICapabilityAdapter {
    // Grant the capability to `consumer`, or revert if it cannot be granted.
    // Called by the broker AFTER settlement and BEFORE payout.
    function grant(address consumer, bytes calldata params) external returns (bytes memory receipt);

    // Read-only discovery: the price and the token a grant is priced in.
    function quote(bytes calldata params) external view returns (uint256 amount, address payToken);
}
Adapters are not trusted with funds. The broker holds and distributes USDC; the adapter only decides “grant this consumer, yes or no?” and returns an opaque receipt the broker surfaces in its Brokered event. The full ABI is in the IAdapter reference.

The security model

Three rules every shipped adapter follows. They are what makes a permissionless broker safe.
  1. Gate grant to the broker. A permissionless grant is a free-mint and payment-bypass hole: anyone could obtain the capability without paying. Only the broker, which settles payment before calling grant, may grant. Store the broker address, restrict grant with an onlyBroker modifier, and expose setBroker(address) for redeploys. This keeps the primitive permissionless (anyone still relays through the broker) while closing the hole.
  2. Enforce scarcity on-chain, and revert when unavailable. A revert inside grant unwinds the broker’s settlement atomically, so a consumer is never charged for a capability that did not exist. AllocationAdapter reverts AllocationSoldOut; TimeboostAdapter reverts NoLaneControl.
  3. Checks-effects-interactions. Commit your effects before any external interaction, so a reentrant call cannot exceed the cap. The broker’s nonReentrant is the backstop; the adapter does its own ordering as well.
You are paid amount - fee at payee, after the grant, by the broker in the same transaction. The relayer (msg.sender) earns fee = amount * feeBps / 10000.
Pricing is two parts, and only one is yours. Your quote sets amount (the slot price and the payToken). The consumer chooses feeBps, which is the relayer’s incentive, not your revenue. Do not expect the fee to be yours: you receive amount - fee.

Worked example A: AllocationAdapter

The guaranteed floor: capped allocation slots in a tokenized-stock vault, with no oracle, no sequencer, and no off-chain service in its path. It is is ICapabilityAdapter, ERC721, Ownable, holds cap / allocated / slotVault / nextSlotId, and is constructed with (payToken, slotPrice, broker, owner). The grant shows all three rules at once: gated to the broker, scarcity checked then reverted on sold-out, effects committed before the mint.
function grant(address consumer, bytes calldata params) external onlyBroker returns (bytes memory) {
    uint256 vaultId = abi.decode(params, (uint256));
    if (allocated[vaultId] >= cap[vaultId]) revert AllocationSoldOut(vaultId);

    // EFFECTS, committed before the INTERACTION below, so a reentrant
    // onERC721Received cannot exceed the cap.
    allocated[vaultId] += 1;
    uint256 slotId = ++nextSlotId;
    slotVault[slotId] = vaultId;
    emit SlotAllocated(consumer, vaultId, slotId);

    // INTERACTION: mint the rivalrous allocation position to the consumer.
    _safeMint(consumer, slotId);

    return abi.encode(slotId);
}

function quote(bytes calldata) external view returns (uint256, address) {
    return (slotPrice, payToken);
}
Stage and price it, off the settlement path. The repository’s Foundry scripts wire the broker and the adapters: Deploy.s.sol plus DeployAdapters.s.sol on Arbitrum Sepolia, and DeployRobinhood.s.sol on Robinhood. The decision you make is the constructor, (payToken, slotPrice, broker, owner). Then setCap(vaultId, n) opens real scarcity, and you can mint TUSDC to test consumers. setCap and setBroker are onlyOwner admin on the caps and wiring; they never touch the broker’s settlement logic. The full ABI and addresses are in the IAdapter reference and Deployments.

Worked example B: TimeboostAdapter

A categorically different scarcity: priority inclusion in a single Arbitrum Timeboost express lane, 60-second rounds, use-it-or-lose-it. That is a third shape beside Allocation (a consumable cap) and Gas (execute-my-action). Running two categorically distinct adapters side by side is what proves the primitive is general, not specific to one capability. It is is ICapabilityAdapter, Ownable, holds auction / laneOperator / slotPrice / payToken / broker, and is constructed with (auction, laneOperator, slotPrice, payToken, broker, owner). Its grant guards on live round control, then commits to the consumer’s exact payload.
function grant(address consumer, bytes calldata params) external onlyBroker returns (bytes memory) {
    uint64 round = auction.currentRound();
    (IExpressLaneAuction.ELCRound memory a, IExpressLaneAuction.ELCRound memory b) = auction.resolvedRounds();
    bool controlled = (a.round == round && a.expressLaneController == laneOperator)
        || (b.round == round && b.expressLaneController == laneOperator);
    if (!controlled) revert NoLaneControl();

    bytes32 commitment = keccak256(params);
    emit InclusionGranted(consumer, commitment, round);
    return abi.encode(commitment);
}
The honest frontier. The payment and the round-control guard are on-chain, in this adapter. The real inclusion is off-chain, via the el-proxy (agents/timeboost): it wraps the consumer’s raw transaction in an express-lane envelope signed by the laneOperator key and submits timeboost_sendExpressLaneTransaction on the sequencer RPC. The contract proves it controls the current round and commits to a payload; it does not perform the inclusion. The el-proxy honors the on-chain InclusionGranted commitment.
Do not use transferExpressLaneController. Nitro ignores it in the initial release: the call succeeds, but the sequencer disregards it. The correct pattern is the controller signing envelopes that wrap third parties’ transactions (the el-proxy). This adapter therefore never writes to the auction; it only reads currentRound() and resolvedRounds() to guard.
The Timeboost resale market has largely collapsed. An empirical study of 11.5M transactions (arXiv:2509.22143) found two entities winning over 90% of auctions and roughly 22% of transactions reverting. Timeboost is in Caplane as a generality proof, the categorically different second adapter and an Arbitrum-native capability, not as a claim of live resale demand. See honest limits. The guard reads the live ExpressLaneAuction at 0x991DbEDf…527D (Arbitrum Sepolia, 60-second rounds, reserve 1 wei).

Make it discoverable

Discovery is opt-in and entirely off the settlement path. Two steps, both optional for settlement.
  • Add the capability to capabilities.json so the consumer SDK can target it by name. The entry maps the capability to { chainId, contract, paramsSchema }; resolveCapability then reads the adapter address from that chain’s registry, so the address is never duplicated.
  • Optionally register on the CapabilityRegistry, signed from your own wallet, so an indexer (and a marketplace surface) can enumerate your capability.
function register(
    uint256 chainId,
    address adapter,
    address provider,        // the payee you name; display only
    bytes32 capabilityKind,  // e.g. keccak256("allocation"); a label, not a permission
    string calldata paramsSchema,
    string calldata label,
    string calldata agentId  // optional ERC-8004 id, "" if unset
) external;
Registering is discovery, not permission. The broker never gates on it. The capability settles through the broker whether or not it is listed; the registry holds no funds, gates no settlement, and cannot affect a grant. A listing is self-scoped, keyed by keccak256(abi.encode(chainId, adapter, msg.sender)), so no one can overwrite yours, and a false listing can only mislead a display, never misroute a payment. The consumer signs the real adapter, payee, and params into the order hash, which never touches this contract.
You deploy your adapter, the consumer signs your address into their order, and the broker calls you. No one approves you into the market.

Verify

Deploy the adapter, set a cap, and have a consumer broker an order against your address (see the agent-builder lane). The grant mints or commits, the Brokered event carries your adapter and payee, and you receive amount - fee at payee in the same transaction. If you set the cap to zero first, the order reverts AllocationSoldOut and the consumer is not charged: that is the atomic unwind, observable.