Skip to content
Beam Docs
Integrations

Chainlink CRE

Start Beam transfers from a smart contract through the Chainlink Runtime Environment, and record DON-attested Beam reports of finished transfers onchain.

The Chainlink Runtime Environment (CRE) runs workflows on a Chainlink decentralized oracle network (DON). This integration lets a smart contract use Beam as its data-movement layer:

  • Start a transfer. A contract call asks for a transfer, and a CRE workflow starts it through a Beam Studio workflow you configured.
  • Record a receipt. When the transfer finishes, a CRE workflow reads Beam's report of it and writes a receipt onchain: status, size, completion time and fingerprints, signed by the DON.
  • Deliver to Room members. A request can also deliver a file to members of a Beam Room, such as private agents or compute nodes, and record a receipt that each target agent signed.
  • Ask a private agent for a job. A request can send a job's input to a compute agent in a Room and record onchain that the input reached it and that its output was delivered back. See Jobs.

Payloads never go near the chain or the DON. Only metadata about the transfer does.

Availability

The relay, for EVM chains CRE does not serve such as Bittensor EVM, is available: it starts transfers, delivers to Room members and records Beam-reported receipts and job results. The CRE workflows have run end to end in Chainlink's CRE simulator; running them on a Chainlink DON needs CRE deploy access, which Beam has not used yet.

The code is the packages/cre directory of the Beam SDK repository: a Solidity contract, two CRE TypeScript workflows, a relay for chains CRE does not serve, and a bridge for attestations inside a Room. Its README has every configuration key; this page explains how the parts fit and what a receipt means.

What a receipt proves

A receipt is a DON-attested Beam report: every signing Chainlink node read the same answer from Beam. It shows what Beam reported, not that Beam is right. Room receipts can also carry target-signed receipts, signatures from the agents that received the file. Neither proves that the content is correct. See Trust levels.

The parts

PartWhat it does
BeamTransferReceipts contractStores receipts, and a bounded set of open requests that the workflows answer. Extends Chainlink's ReceiverTemplate.
start-transfer workflowOn a TransferRequested log, posts the request to a Beam Studio webhook trigger.
transfer-receipts workflowReads finished transfers and Room deliveries from Beam by id and writes their receipts. Also answers a Room bridge.
RelayDoes both workflows' jobs on EVM chains CRE does not serve, such as Bittensor EVM, without the DON.
Room bridgeLets Room members ask for an attestation in a Room message and receive the DON-signed report as a Room reply.
Example compute agentA toy Room member that answers jobs: it computes over a delivered file and has Studio deliver the result.

Starting a transfer

requestTransfer(requestId, 1, input) asks for a standard transfer. The requestId is 32 bytes you choose, unique per request. The input string is passed to Studio as is; it is public, like everything onchain.

The start-transfer workflow sends the request to a Studio workflow that you approved in advance. The source and destination are fixed in that workflow, so a request can only start the transfers it describes:

  1. A webhook trigger that requires a signed body, with no coalescing window.
  2. One @beam/transfer step whose idempotencyKey input is bound to ${workflow.input.requestId}.

The workflow signs its delivery with the trigger's signing secret and sets Idempotency-Key to the request id. Every DON node sends the request, and Studio answers repeats with the run it already started (Studio v1.0.20 or later), so one request starts one run and one transfer.

Beam derives the transfer's id from the request: the first 16 bytes of sha256("beam-transfer:<clientId>:<requestId>"), as a version 5 UUID, where clientId is the Beam identity of the key that starts the transfer. That is how the receipts workflow finds the transfer later without being told its id.

Receipts

Standard receipt, keyed by the transfer id: requestId, status (completed, failed or cancelled), totalBytes, bytesTransferred, completedAtMs, planFingerprint, coordinateChecksum and level.

Room receipt, keyed by keccak256(abi.encode(room, channel, publication)): status (completed, partial, failed or cancelled), fileSizeBytes, manifestSha256, targetCount, evidenceSha256, assignmentsSha256, evidenceVersion and level.

BeamTransferReceipts.StandardReceipt memory receipt = receipts.getReceipt(transferId);
bool finished = receipt.status == BeamTransferReceipts.Status.Completed;
bool attested = (receipt.level & receipts.LEVEL_DON_ATTESTED()) != 0;

Every value comes from Beam. A request only names what to look up; the workflow fetches it by id, and only terminal transfers are recorded. A recorded receipt is never overwritten.

How a receipt gets recorded

The contract keeps an open-request set, at most 256 entries, of questions not answered yet. The transfer-receipts workflow answers them three ways:

TriggerWhen it runs
EVM logrequestReceipt(transferId) or requestRoomReceipt(room, channel, publication) asks for one receipt now.
Cron reconcileEvery few minutes, it checks a window of the open-request set and records every finished request. A request whose other paths failed is still answered.
HTTPA push from another workflow, or attestation requests from a Room bridge.

Reconcile reads the open set at the latest finalized block, so a new request waits for finality first: about 16 minutes on Ethereum Sepolia. staleRequests() lists requests older than the deployment's staleAfter; a non-empty list means a request needs attention, such as one naming a transfer that was never created. cancelRequest removes it.

Only accounts the owner allows with setRequester can open requests, since each one spends the operator's Beam reads and CRE quota.

Trust levels

level is a bitmask. Each bit is a claim with a limit.

LevelBitProvesDoes not prove
L1 Beam-reported0x01Beam's API reported this status, size and plan fingerprint.That the bytes arrived intact; that Beam reports honestly.
L2 Target-signed receipt (Room)0x02The agent holding receipt key K signed manifest_sha256 over the range hashes it received.That K belongs to member M, which is a Beam-attested member identity; that the content is correct. For end-to-end encrypted Rooms the hashes cover ciphertext.
L3 DON-signed report0x04Each signing Chainlink node fetched Beam's evidence itself and got the same answer, and the onchain write is authentic.That Beam's evidence is authentic. A wrong Beam answer would be attested faithfully: L3 adds no independent check of delivery.
  • Standard receipts are L1 + L3, level = 5.
  • Room receipts are L1 + L3 (5), plus L2 (7) only when every target is an agent with a signed final receipt. A storage target is finalized by its provider, which Beam reports, so a Room receipt with any storage target stays at 5.
  • The contract does not check target signatures. evidenceSha256 commits to the exact evidence bytes, so anyone holding them can.
  • A relay deployment never records L3. See Chains without CRE.
  • A job result carries only the claims both of its Room receipts hold. See Jobs.

Room deliveries

requestTransfer(requestId, 2, input) asks for a Room delivery: a file published from a source agent to target members of a Room, through a second Studio workflow.

  • The Studio Room workflow has a webhook trigger that requires signatures, and one @beam/room-transfer step whose configuration fixes the Room, the object channel, the source agent and its file, and the targets. Its publicationKey input is bound to ${workflow.input.requestId}, so the delivery is published under the key request:<requestId>. This needs Studio v1.0.20 or later.
  • start-transfer sends Room delivery requests to that workflow's webhook when startRoomDeliveries is on.
  • transfer-receipts finds the delivery from the request: with the Room, channel and source member in its roomDelivery config, reconcile looks the publication up by its key, then reads its Room receipt and records it with the request id. Studio never has to report back.

What the receipt claims, exactly: the configured targets received the file whose manifest it names, and with a signed receipt from every target it is L2. It says nothing about whether the content is correct. A delivery from or to object storage runs as a storage transfer and is not found this way.

The Room receipt

The transfer-receipts workflow reads a terminal Room transfer's evidence from the Room coordinator:

GET /studio/v1/rooms/{room}/channels/{channel}/objects/{publication}/receipt

The answer is the canonical beam-room-receipt-evidence/v1 document: RFC 8785 (JCS) JSON, byte-identical on every read, holding the ids, the status, the manifest hash, and each target with its signed final receipt. Every DON node hashes the raw bytes, so the nodes agree without re-serializing anything:

  • evidenceSha256 = sha256("beam:room-receipt-evidence:v1" 0x00 ‖ the document);
  • assignmentsSha256 commits to the complete list of chunk assignments. Beam computes it, and the list is served page by page at …/receipt/assignments?page=N.

A published test vector fixes both hashes, and the package's checks reproduce its evidenceSha256 from the document's bytes.

Jobs

requestTransfer(requestId, 3, input) asks for a job: Beam delivers the job's input to a compute member of a Room, such as a private AI agent, and the request is answered once that member's output has been delivered too. input carries the instruction, and like everything onchain it is public.

A job needs two Studio Room workflows, each with a webhook trigger that requires signatures and one @beam/room-transfer step whose publicationKey input is bound to ${workflow.input.requestId}:

  • The input workflow sends a fixed file from a source agent to the compute member. start-transfer sends jobs to it when startJobs is on.
  • The output workflow sends the compute member's result file, also a fixed path, to a receiving member. The compute agent starts it when its result is ready.
  • The compute member is a Studio agent allowed to read its result file, with subscribe on the input channel and publish on the output channel.
  • transfer-receipts finds both deliveries: roomDelivery names the input's channel and source member, and jobOutput names the output channel and the compute member. A job costs four lookups, so reconcileBatchSize is at most 3.
  • Both object channels need a publication TTL at least as long as the Studio steps' TTL.

A Room member that receives a file never sees its publication key, so the compute agent learns the request id from the chain. The example agent in packages/cre/examples/compute-agent follows the contract's TransferRequested logs of kind 3. It takes the next input that reaches its Room inbox, computes, writes its result file and starts the output workflow with the request id, serving jobs one at a time.

When both deliveries have finished, one report records their two Room receipts and the job result. getJob(requestId) returns:

  • the Room receipt keys and manifest hashes of the input and the output;
  • the compute member;
  • status: the input's, or the output's once the input completed;
  • level: only the claims both Room receipts carry, so 7 on a CRE deployment when every target of both deliveries signed its receipt.

What a job result claims

The agent holding receipt key K received input manifest A, and output manifest B was delivered from member M. That K belongs to M is Beam-attested. A job result never claims that the output is correct.

Attestations in a Room

A Room member can ask for the same DON-signed answer without leaving the Room. The bridge runs as a webhook member of the Room, next to its host agent. Ordinary Room traffic stays member to member and never reaches Chainlink; only these requests do.

  1. Ask. A member publishes a message on a channel the bridge listens to:
    {"op": "attest_room_publication", "channel": "btr_channel_…", "publication": "btr_pub_…"}
    or {"op": "attest_transfer", "transfer_id": "…"}. A publication is looked up only in the asking member's own Room, and only in channels the operator lists. publish_onchain: true also records the answer in the contract.
  2. Accept. The host delivers the message with an HMAC signature. The bridge answers with an asynchronous reply, which the host polls for.
  3. Attest. The bridge sends pending requests to the transfer-receipts workflow's HTTP trigger, at most seven per call and one call per 30 seconds. For each finished subject, the workflow sends the bridge one DON-signed report, so a reply in one Room carries nothing about another.
  4. Reply. The bridge checks the report's workflow id and owner, then f+1 signatures from distinct signers of the report's DON, read from Chainlink's Capability Registry on Ethereum mainnet. The host publishes the reply in the Room, one hop after the request.

The reply carries the answer, the verified signers, the workflow's identity and the raw signed report. Members do not have to trust the bridge: the package's verify.ts checks a reply's signatures against the registry itself and decodes the answer from the signed bytes. Other op values get a refusal. A result sent as a message cannot be attested, because the DON only attests what it reads from Beam: publish the result as a Room transfer and ask about that publication.

Chains without CRE

CRE serves a fixed set of chains; Bittensor EVM is not one of them. On such a chain, the relay does both workflows' jobs from one process. It follows TransferRequested logs up to the finalized block and starts each request through the same Studio webhook. It looks up open requests at Beam and writes the finished receipts itself. A relay can also run receipts only, with no Studio: contracts then ask for receipts of transfers and Room deliveries that already happened.

Beam runs such a relay on Bittensor EVM mainnet, receipts only, and records receipts for Beam customers at 1 credit each: see Bittensor EVM.

A relay is one operator, so its receipts are Beam-reported only. Deploy the contract with donAttested false and the relay's account as its forwarder: standard receipts then record level = 1 and Room receipts 1 or 3, and the contract refuses any report that claims DON attestation. donAttested() says which kind of deployment a contract is.

Room members can ask questions on such a chain too. With its answers taken from the contract, the bridge opens each request on the contract, the relay records the receipt like any other, and the reply carries the contract's record and the transaction that recorded it. What that proves is exactly what the relay operator recorded, which anyone can read from the contract: no DON or other independent party attests it, and the reply says so. The package's verify.ts reads the receipt from the contract and checks that the reply says the same.

API keys

The workflows, the relay and the bridge's lookups only read. Create their Beam key as an API key (b1m_…) on the console's API keys page, then apply the Read-only: transfers + Room evidence preset in that key's permission matrix. The preset holds transfers:read and room:objects:read, so the key cannot create transfers or change Rooms. A service account credential (bm_sa_…) does not work here: it administers keys and billing through the management API, and BeamCore accepts only API keys. Create it in the organization whose key starts the transfers: every key an organization issues belongs to one Beam identity, so both keys share the client id transfer ids derive from.

On a deployed workflow the key lives in Chainlink's Vault DON and is fetched per execution. The workflow sends it only to Beam's API and Room coordinator.

Onchain data is public

Everything the contract stores or emits is public and permanent: transfer ids, request ids, byte counts, completion times, fingerprints, Room, channel and publication ids, manifest and evidence hashes, and the input string of requestTransfer. Payloads and credentials never go onchain. Do not put personal data or secrets in input, or in the ids of Rooms and channels you ask about.

Quotas

CRE limitValueHow the workflows fit
HTTP requests per execution15At most 14 lookups, plus one identity check. A Room delivery request costs two, a job four, and an attestation two: its read and the delivery of its report.
Report payload50 KB14 receipts encode to about 12 KB.
HTTP trigger rate1 per 30 sBatch pushes; the bridge calls at most once per 30 s.
Workflows per organization3transfer-receipts and start-transfer use two.

Reconcile records about 14 requests per run, 168 per hour at the default 5-minute interval. A full open set of 256 takes about 95 minutes to work through.

Setup

You need Bun 1.2.21 or later, Foundry, the CRE CLI, a Beam API key, and testnet ETH to deploy the contract.

cd packages/cre/workflows/transfer-receipts
bun install
bun run typecheck
bun run check

cd ../../contracts/evm
forge build
forge test

Simulate from packages/cre with a CRE login; simulation needs no approval:

# Reconcile the open requests
cre workflow simulate workflows/transfer-receipts --target staging-settings \
  --non-interactive --trigger-index 1

# Record one transfer's receipt from an HTTP push
cre workflow simulate workflows/transfer-receipts --target staging-settings \
  --non-interactive --trigger-index 2 \
  --http-payload '{"transfers":[{"transferId":"<uuid>"}]}'

Add --broadcast to write the receipt through the simulation forwarder.

Deploy to the DON once Chainlink has approved deploy access for your organization. Deploy the contract with your chain's production forwarder, restrict it to your workflow with setExpectedAuthor and setExpectedWorkflowId, allow your requesters with setRequester, and store the secrets in the Vault DON with cre secrets create. The package README walks through each step.

Decentralized distributed bandwidth infrastructure.

On this page