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.
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:
A webhook trigger that requires a signed body, with no coalescing window.
One @beam/transfer step whose idempotencyKeyinput 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.
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.
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.
The contract keeps an open-request set, at most 256 entries, of questions not
answered yet. The transfer-receipts workflow answers them three ways:
Trigger
When it runs
EVM log
requestReceipt(transferId) or requestRoomReceipt(room, channel, publication) asks for one receipt now.
Cron reconcile
Every 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.
HTTP
A 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.
level is a bitmask. Each bit is a claim with a limit.
Level
Bit
Proves
Does not prove
L1 Beam-reported
0x01
Beam's API reported this status, size and plan fingerprint.
That the bytes arrived intact; that Beam reports honestly.
L2 Target-signed receipt (Room)
0x02
The 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 report
0x04
Each 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.
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 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.
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.
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.
Ask. A member publishes a message on a channel the bridge listens to:
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.
Accept. The host delivers the message with an HMAC signature. The bridge
answers with an asynchronous reply,
which the host polls for.
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.
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.
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.
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.
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.
At 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 payload
50 KB
14 receipts encode to about 12 KB.
HTTP trigger rate
1 per 30 s
Batch pushes; the bridge calls at most once per 30 s.
Workflows per organization
3
transfer-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.
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-receiptsbun installbun run typecheckbun run checkcd ../../contracts/evmforge buildforge test
Simulate from packages/cre with a CRE login; simulation needs no approval:
# Reconcile the open requestscre workflow simulate workflows/transfer-receipts --target staging-settings \ --non-interactive --trigger-index 1# Record one transfer's receipt from an HTTP pushcre 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.