Building distributed object-capability systems requires solving a fundamental challenge: how do objects in different isolated compartments or machines communicate safely and asynchronously? The Endo stack provides a layered solution through four interconnected packages:
Together, these packages implement safe message passing: the ability to send messages to objects that will receive and process them, with strong safety guarantees at every step.
This guide presents a natural progression: from data (what can be passed?) through validation (is it well-formed?) to objects (how do we receive safely?) and finally communication (how do we pass messages?). Each concept builds on the previous one, culminating in a complete example showing all four packages working together.
This guide assumes familiarity with:
lockdown() functionBefore we can send messages between isolated compartments, we need to
understand what data can safely cross boundaries.
The @endo/pass-style package defines the Passable type and classifies
values according to their PassStyle.
What is a vat? Throughout this guide, we use the term vat to refer to an isolated unit of computation—an idealization of a worker, in the same sense as a Turing Machine as an idealization of computation. A vat has its own heap, stack, and event queue. It communicates with other vats exclusively through asynchronous message passing, with no shared memory concurrency. Events run serially to completion: each event begins with an empty stack and a guarantee that no other code that shares mutable state will be interleaved during its execution. This model achieves parallelism through multiple vats and achieves performance by transferring ownership of memory (such as ArrayBuffers) between vats when necessary. Otherwise, this model abandons some opportunities to improve performance and, in exchange, obviates any possiblity of deadlock, even in composition of many, independently designed compoennts, utterly unaware of each other's synchronization models. This is the linchpin of fearless cooperation among components in a loosely-coordinated, distributed, multi-agent system, which is to say, any system at sufficient scale.
Every passable value falls into one of eight categories:
| Pass Style | Description | Examples |
|---|---|---|
'null' |
The null value | null |
'undefined' |
The undefined value | undefined |
'boolean' |
Boolean primitives | true, false |
'number' |
IEEE 754 floats | 42, 3.14, NaN, Infinity |
'bigint' |
Arbitrary integers | 123n, -456n |
'string' |
Well-formed strings | 'hello', '' |
'symbol' |
Registered/well-known symbols | Symbol.iterator |
'copyArray' |
Frozen arrays of passables | harden([1, 2, 3]) |
'copyRecord' |
Frozen plain objects | harden({ x: 10 }) |
'remotable' |
Far objects & presences | Far('Counter', {...}) |
'tagged' |
Extension point for domain types | makeTagged('copySet', [...]) |
'error' |
Error objects | harden(Error('failed')) |
'promise' |
Promise objects | Promise.resolve(42) |
The key distinction is between pass-by-copy (the value itself is copied) and pass-by-reference:
The @endo/pass-style package provides core functions for inspecting
the pass-style of a value.
import { passStyleOf, isPassable, Far, makeTagged, passableSymbolForName } from '@endo/pass-style';
// Classify a value's pass style
passStyleOf(42); // 'number'
passStyleOf(harden([1, 2])); // 'copyArray'
passStyleOf(Promise.resolve()); // 'promise'
// Check if a value is passable
isPassable({ x: 1 }); // false (not frozen)
isPassable(harden({ x: 1 })); // true
// Create passable symbols
const mySymbol = passableSymbolForName('mySymbol');
passStyleOf(mySymbol); // 'symbol'
It also provides a Far utility function for making "remotables", values that
can receive messages, which we use in this example to demonstrate
passStyleOf, but note that it provides no protection against invalid argument
patterns, which we will remedy with makeExo and type guards farther along.
// Create a remotable object
const counter = Far('Counter', {
increment(n) { return n + 1; },
getValue() { return 42; }
});
passStyleOf(counter); // 'remotable'
A value is passable if it meets these requirements:
harden()passableSymbolForName() from
@endo/pass-style.// This is passable - frozen array of primitives
const data = harden([1, 2, 3]);
// This is NOT passable - not frozen
const mutable = [1, 2, 3];
passStyleOf(mutable); // throws Error
// This is NOT passable - cyclic reference
const cyclic = harden([]);
cyclic.push(cyclic);
passStyleOf(cyclic); // throws Error
The Far() function creates remotable objects that can be passed by reference:
import { Far } from '@endo/pass-style';
const makeCounter = (initialValue = 0) => {
let count = initialValue;
return Far('Counter', {
increment() {
count += 1;
return count;
},
getValue() {
return count;
}
});
};
const counter = makeCounter(5);
// This counter is a remotable - it can be passed as a reference
// but its internal state (count) remains private
Key Insight: Far objects provide encapsulation and can be passed across
boundaries, but they don't validate their inputs.
If increment() expects a number but receives a string, it won't detect the
error until the method executes.
This is where patterns come in.
Now we can pass objects between vats, but how do we ensure received data is well-formed? The @endo/patterns package provides pattern matching to validate passable data and describe behavioral contracts.
The M namespace offers matchers for all pass styles:
import { M, matches, mustMatch } from '@endo/patterns';
// Primitive matchers
M.any(); // Matches any passable
M.number(); // Matches any number
M.string(); // Matches any string
M.boolean(); // Matches any boolean
// Constrained matchers
M.gte(0); // Matches numbers >= 0
M.string({ maxSize: 100 }); // Matches strings up to 100 chars
M.nat(); // Matches non-negative bigints
// Container matchers
M.array(); // Matches any copyArray
M.record(); // Matches any copyRecord
M.arrayOf(M.number()); // Matches arrays of numbers only
M.recordOf(M.string(), M.number()); // Matches {string: number} records
// Logical operators
M.and(M.number(), M.gte(0)); // Matches non-negative numbers
M.or(M.string(), M.number()); // Matches strings or numbers
M.opt(M.string()); // Matches undefined or string (optional)
The matches() function tests if a value matches a pattern:
import { M, matches, mustMatch } from '@endo/patterns';
const pattern = M.and(M.number(), M.gte(0), M.lte(100));
matches(50, pattern); // true
matches(-10, pattern); // false
matches('50', pattern); // false
// mustMatch() throws with a descriptive error
mustMatch(42, M.string());
// throws: "number 42 - Must be a string"
For more complex validation, M.splitRecord() allows you to specify required
properties, optional properties, and a pattern for any remaining properties:
import { M, mustMatch } from '@endo/patterns';
// Define a pattern with required and optional properties
const UserPattern = M.splitRecord(
{ name: M.string() }, // required properties
{ age: M.number(), email: M.string() }, // optional properties
M.string() // rest properties must be strings
);
// Valid: has required 'name'
const user1 = harden({ name: 'Alice' });
mustMatch(user1, UserPattern); // passes
// Valid: has required and optional properties
const user2 = harden({ name: 'Bob', age: 30 });
mustMatch(user2, UserPattern); // passes
// Valid: has required, optional, and extra properties
const user3 = harden({ name: 'Carol', age: 25, bio: 'Engineer' });
mustMatch(user3, UserPattern); // passes (bio matches rest pattern)
// Invalid: missing required 'name'
const user4 = harden({ age: 30 });
mustMatch(user4, UserPattern); // throws: missing required property 'name'
// Invalid: rest property is not a string
const user5 = harden({ name: 'Dave', score: 100 });
mustMatch(user5, UserPattern); // throws: rest property 'score' must be string
This pattern is particularly useful for validating configuration objects, method arguments, and data structures where you want to enforce required fields while allowing optional extensions.
Patterns introduces three passable collection types built on makeTagged():
A set of unique Keys (primitives or remotables):
import { makeCopySet } from '@endo/patterns';
const colors = makeCopySet(['red', 'blue', 'green']);
// Elements are sorted and deduplicated
// Can contain strings, numbers, remotables, etc.
// Pattern for sets
const ColorSet = M.setOf(M.string());
mustMatch(colors, ColorSet); // passes
A multiset (elements with counts):
import { makeCopyBag } from '@endo/patterns';
const inventory = makeCopyBag([
['apples', 5n],
['oranges', 3n]
]);
const InventoryPattern = M.bagOf(M.string(), M.bigint());
mustMatch(inventory, InventoryPattern);
A map from Keys to passable values:
import { makeCopyMap } from '@endo/patterns';
// Map user IDs to balances
const balances = makeCopyMap([
['alice', 100],
['bob', 50]
]);
const BalancesPattern = M.mapOf(M.string(), M.number());
mustMatch(balances, BalancesPattern);
Why not use plain objects or arrays?
CopyMap/CopySet/CopyBag support efficient key comparison using compareKeys(),
enable partial ordering for subset relationships, and are explicitly designed
as passable data structures.
Patterns can also describe behavioral contracts through InterfaceGuards:
import { M } from '@endo/patterns';
const CounterI = M.interface('Counter', {
// Method signature: call with number, returns number
increment: M.call(M.number()).returns(M.number()),
// Method with no arguments
getValue: M.call().returns(M.number()),
// Method with optional arguments
reset: M.call().optional(M.number()).returns()
});
This describes a contract: the Counter interface has three methods with specific argument and return types. But how do we enforce this contract automatically?
Key Insight: Patterns describe what we expect, but don't enforce it by themselves. We need a way to wrap objects so they validate inputs against patterns before executing methods. Enter exo.
Exos introduce interface guards for remotable objects.
At this point, you should forget about the Far stepping-stone and always
immediately reach or exos.
The @endo/exo package turns defensive programming from a manual discipline
into an automatic guarantee.
An exo is a remotable object protected by an InterfaceGuard. When a method is called, the guard validates arguments against their patterns before the method executes:
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
const CounterI = M.interface('Counter', {
increment: M.call(M.number()).returns(M.number())
});
let count = 0;
const counter = makeExo('Counter', CounterI, {
increment(n) {
// By the time we reach here, n is guaranteed to be a number
count += n;
return count;
}
});
// Valid call
counter.increment(5); // returns 5
// Invalid call - caught by guard
counter.increment('5');
// throws: "(Counter).increment(string \"5\") - Must be a number"
The guard provides the first layer of defense against malformed and malicious input. Your method implementation can focus on business logic, not type-checking.
Exo provides three patterns depending on your needs:
Use when you need one instance with no state management:
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
const GreeterI = M.interface('Greeter', {
greet: M.call(M.string()).returns(M.string())
});
const greeter = makeExo('Greeter', GreeterI, {
greet(name) {
return `Hello, ${name}!`;
}
});
Use when you need multiple instances, each with their own state:
import { defineExoClass } from '@endo/exo';
import { M } from '@endo/patterns';
const CounterI = M.interface('Counter', {
increment: M.call().optional(M.number()).returns(M.number()),
getValue: M.call().returns(M.number())
});
const makeCounter = defineExoClass(
'Counter',
CounterI,
// init function: creates initial state
(initialValue = 0) => ({ count: initialValue }),
// methods: have access to this.state and this.self
{
increment(delta = 1) {
const { state } = this;
state.count += delta;
return state.count;
},
getValue() {
return this.state.count;
}
}
);
const counter1 = makeCounter(0);
const counter2 = makeCounter(100);
counter1.increment(); // 1
counter2.increment(); // 101
Use when you need multiple related objects (facets) sharing the same state. This is the cornerstone of the principle of least authority: give each client only the facet they need.
import { defineExoClassKit } from '@endo/exo';
import { M } from '@endo/patterns';
const CounterI = {
up: M.interface('UpCounter', {
increment: M.call(M.number()).returns(M.number())
}),
down: M.interface('DownCounter', {
decrement: M.call(M.number()).returns(M.number())
}),
reader: M.interface('CounterReader', {
getValue: M.call().returns(M.number())
})
};
const makeCounterKit = defineExoClassKit(
'Counter',
CounterI,
// init: shared state across all facets
(initialValue = 0) => ({ count: initialValue }),
// methods: one object per facet
{
up: {
increment(delta) {
this.state.count += delta;
return this.state.count;
}
},
down: {
decrement(delta) {
this.state.count -= delta;
return this.state.count;
}
},
reader: {
getValue() {
return this.state.count;
}
}
}
);
const { up, down, reader } = makeCounterKit(50);
// Give clients only the facets they need
// incrementer only gets `up`, decrementer only gets `down`
// but both affect the same shared state
up.increment(10); // 60
down.decrement(5); // 55
reader.getValue(); // 55
For methods that await promises, use M.callWhen() to mark them as
asynchronous:
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { E } from '@endo/eventual-send';
const FetcherI = M.interface('Fetcher', {
// Async method: awaits the url argument, returns promise
fetch: M.callWhen(M.string()).returns(M.string())
});
const fetcher = makeExo('Fetcher', FetcherI, {
async fetch(url) {
// url is validated, then awaited if it's a promise
const response = await E(httpClient).get(url);
return response.text();
}
});
The M.callWhen() guard:
Each exo pattern handles state differently:
this.state is per-instance, this.self is the exo
itselfthis.state shared across facets, this.facets
contains all facetsconst makeWallet = defineExoClass(
'Wallet',
WalletI,
(initialBalance) => ({ balance: initialBalance }),
{
deposit(amount) {
// Access state
this.state.balance += amount;
// Return self for chaining
return this.self;
},
withdraw(amount) {
if (amount > this.state.balance) {
throw Error('Insufficient funds');
}
this.state.balance -= amount;
return amount;
}
}
);
Every exo has a special meta-method to retrieve its interface:
import { GET_INTERFACE_GUARD } from '@endo/exo';
import { getInterfaceMethodKeys } from '@endo/patterns';
const counter = makeCounter();
// Get the interface guard
const guard = counter[GET_INTERFACE_GUARD]();
// Inspect available methods
const methods = getInterfaceMethodKeys(guard);
console.log(methods); // ['increment', 'getValue']
This enables runtime introspection and dynamic client generation.
Key Insight: Exos provide safe "receive" semantics - when messages arrive at an exo, the InterfaceGuard validates inputs before execution. But we still need a uniform way to send messages, whether the exo is local or remote. That's where eventual-send comes in.
The @endo/eventual-send package provides the E() proxy for asynchronous
message passing.
Whether an object is in the same vat, a different vat, or across a network,
E() provides a uniform API that always returns promises.
The E() function creates a proxy that intercepts method calls and property
access:
import { E } from '@endo/eventual-send';
// Local exo object
const counter = makeCounter(0);
// Eventual send: invoke method, get promise
const resultP = E(counter).increment(5);
// resultP is a promise, even though counter is local
const result = await resultP; // 5
Even for local objects, E() introduces asynchrony by deferring the method
call to the next turn of the event loop.
This provides:
Eventual send: invoke a method, returning a promise for the result.
const counter = makeCounter(10);
// Send message, get promise
const result = await E(counter).increment(5);
console.log(result); // 15
// Works even if counter is a promise
const counterP = Promise.resolve(counter);
const result2 = await E(counterP).increment(3); // 18
Eventual get: retrieve a property, returning a promise for its value.
const config = harden({
timeout: 5000,
retries: 3
});
const timeoutP = E.get(config).timeout;
const timeout = await timeoutP; // 5000
Fire-and-forget: send message without waiting for result.
Returns undefined immediately, providing an optimization when you don't need
the result.
const logger = makeLogger();
// Send log message, don't wait for result
E.sendOnly(logger).log('Event occurred');
// Returns immediately, logging happens eventually
Shorthand for promise handling with turn tracking:
E.when(
E(counter).getValue(),
value => console.log('Value:', value),
error => console.error('Error:', error)
);
One of the most powerful features of eventual-send is promise pipelining: the ability to send messages to promises before they resolve.
import { E } from '@endo/eventual-send';
// mintP is a promise for a mint
const mintP = E(bootstrap).getMint();
// We can send messages to mintP immediately
// Don't need to await it first!
const purseP = E(mintP).makePurse();
// Chain more messages
const balanceP = E(purseP).getBalance();
// All messages are sent immediately
// They execute in order when each promise resolves
const balance = await balanceP;
Without pipelining, you'd need to await each step:
// Without pipelining: 3 round trips
const mint = await bootstrap.getMint(); // wait
const purse = await mint.makePurse(); // wait
const balance = await purse.getBalance(); // wait
// With pipelining: messages sent immediately
const balance = await E(E(E(bootstrap).getMint()).makePurse()).getBalance();
// Only wait at the end
This can dramatically reduce latency in distributed systems.
Eventual send provides four key benefits:
// This code works unchanged whether counter is:
// - A local exo object
// - A local promise for an exo
// - A remote presence in another vat
// - A remote presence on another machine
const result = await E(counter).increment(5);
Exos are the ideal targets for eventual send:
import { makeExo } from '@endo/exo';
import { M } from '@endo/patterns';
import { E } from '@endo/eventual-send';
const CounterI = M.interface('Counter', {
increment: M.call(M.number()).returns(M.number())
});
const counter = makeExo('Counter', CounterI, {
increment(n) {
return count += n;
}
});
// E() provides async wrapper
const resultP = E(counter).increment(5);
// The InterfaceGuard validates n is a number
// Even if counter is remote, validation happens on receive
Even for local exos, using E() provides benefits:
Key Insight: We now have all the pieces: passable data (pass-style), validation (patterns), defensive objects (exo), and async communication (eventual-send). Let's put them all together in a complete example.
Let's build a complete capability-based payment system that demonstrates all four packages working together. We'll create a mint that can create purses, which can create payments. This pattern is fundamental to digital assets in capability systems.
Our system will have three facets:
All three facets share the same underlying state, but expose different authority levels.
import { M } from '@endo/patterns';
import { defineExoClassKit, defineExoClass } from '@endo/exo';
import { E } from '@endo/eventual-send';
// Step 1: Define interfaces (patterns)
const MintI = M.interface('Mint', {
makePurse: M.call().returns(M.remotable('Purse'))
});
const PurseI = M.interface('Purse', {
getBalance: M.call().returns(M.number()),
deposit: M.callWhen(
M.and(M.number(), M.gte(0)),
M.remotable('Payment')
).returns(),
withdraw: M.call(M.and(M.number(), M.gte(0))).returns(M.remotable('Payment'))
});
const PaymentI = M.interface('Payment', {
getBalance: M.call().returns(M.number())
});
// Step 2: Define the Mint/Purse Kit
const makeMintKit = defineExoClassKit(
'Mint',
{ mint: MintI, purse: PurseI },
// init: each purse starts with 0 balance
() => ({ balance: 0 }),
{
mint: {
makePurse() {
// Return the purse facet, not the mint facet
// This ensures the holder of a purse can't mint
return this.facets.purse;
}
},
purse: {
getBalance() {
return this.state.balance;
},
async deposit(amount, payment) {
// amount is validated as non-negative number by guard
// payment is validated as remotable by guard
// Get payment's balance (eventual send)
const paymentBalance = await E(payment).getBalance();
// Verify amount matches
if (paymentBalance !== amount) {
throw Error('Payment balance mismatch');
}
// Add to our balance
this.state.balance += amount;
},
withdraw(amount) {
// amount is validated as non-negative by guard
if (amount > this.state.balance) {
throw Error('Insufficient balance');
}
// Deduct from balance
this.state.balance -= amount;
// Create a new payment
return makePayment(amount);
}
}
}
);
// Step 3: Define single-use Payment
const makePayment = defineExoClass(
'Payment',
PaymentI,
// init: payment created with specific amount
(amount) => ({ balance: amount, spent: false }),
{
getBalance() {
// Once spent, balance becomes 0
if (this.state.spent) {
return 0;
}
// Mark as spent (single-use)
this.state.spent = true;
return this.state.balance;
}
}
);
// Step 4: Usage across vat boundaries
// Create a mint (privileged)
const { mint, purse: ourPurse } = makeMintKit();
// Give someone else a purse (they can't mint!)
const alicePurse = E(mint).makePurse();
const bobPurse = E(mint).makePurse();
// Manually increase our purse (in real system, this would be privileged)
ourPurse.state.balance = 1000;
// Create a payment and send to Alice
const payment100 = E(ourPurse).withdraw(100);
await E(alicePurse).deposit(100, payment100);
// Alice can now send to Bob
const payment50 = E(alicePurse).withdraw(50);
await E(bobPurse).deposit(50, payment50);
// Check balances (all eventual sends)
const ourBalance = await E(ourPurse).getBalance(); // 900
const aliceBalance = await E(alicePurse).getBalance(); // 50
const bobBalance = await E(bobPurse).getBalance(); // 50
// Try to reuse a payment (fails - single use)
const payment = E(alicePurse).withdraw(10);
await E(bobPurse).deposit(10, payment); // succeeds
await E(ourPurse).deposit(10, payment); // fails - balance is 0
Let's trace the flow when Alice sends money to Bob:
Pass-style: The amount (50) is passable as a number.
The payment is passable as a remotable.
Patterns: When withdraw(50) is called:
50 matches M.and(M.number(), M.gte(0))Exo: The purse exo:
balance) that can't be directly accessedEventual-send: All method calls use E():
Least Authority via Facets: The mint holder has full power to create purses, but purse holders can only deposit/withdraw, not mint new purses.
Single-use Payments: The payment's getBalance() method uses the spent
flag to ensure it can only be deposited once.
This prevents double-spending.
Async Validation: The deposit() method uses M.callWhen() because it
needs to await E(payment).getBalance().
The guard validates the types, then the method validates business logic.
Defense in Depth: Multiple layers of protection:
Uniform Communication: The same code works whether Alice and Bob are:
Now that we've seen the full stack in action, let's discuss patterns for structuring your distributed objects.
Use makeExo when:
// Good use case: stateless utility
const validator = makeExo('Validator', ValidatorI, {
validate(data) {
return checkRules(data);
}
});
Use defineExoClass when:
// Good use case: user sessions
const makeSession = defineExoClass(
'Session',
SessionI,
(userId) => ({ userId, startTime: Date.now() }),
{ /* methods */ }
);
Use defineExoClassKit when:
// Good use case: admin vs user interfaces
const makeService = defineExoClassKit(
'Service',
{ admin: AdminI, user: UserI },
() => ({ data: [] }),
{
admin: { reset() { this.state.data = []; } },
user: { getData() { return this.state.data; } }
}
);
Choose copyable (pass-by-copy) for:
// Copyable: configuration object
const config = harden({
timeout: 5000,
retries: 3,
endpoints: ['api.example.com']
});
// Copyable: message/event
const event = harden({
type: 'transfer',
from: 'alice',
to: 'bob',
amount: 100,
timestamp: Date.now()
});
Choose remotable (pass-by-presence) for:
// Remotable: service with methods
const database = Far('Database', {
query: (sql) => { /* ... */ },
insert: (record) => { /* ... */ }
});
// Remotable: capability
const fileHandle = Far('FileHandle', {
read: () => { /* ... */ },
write: (data) => { /* ... */ }
});
Errors thrown in exo methods are automatically converted to passable form before crossing vat boundaries:
const service = makeExo('Service', ServiceI, {
doOperation(input) {
if (!isValid(input)) {
// This error will be made passable automatically
throw Error('Invalid input');
}
return result;
}
});
// Caller in different vat
try {
await E(service).doOperation(badInput);
} catch (err) {
// err is a passable error
console.error(err.message); // 'Invalid input'
}
Best practices:
Error objects (they're automatically made passable)Use E() even in unit tests for consistency:
import test from 'ava';
import { E } from '@endo/eventual-send';
test('counter increments correctly', async t => {
const counter = makeCounter(0);
// Use E() even though counter is local
const result = await E(counter).increment(5);
t.is(result, 5);
});
This ensures:
For testing remote scenarios, create mock presences:
const mockRemoteService = Far('MockService', {
async getData() {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 100));
return testData;
}
});
Problem:
const FetcherI = M.interface('Fetcher', {
// Wrong: M.call() for async method
fetch: M.call(M.string()).returns(M.string())
});
const fetcher = makeExo('Fetcher', FetcherI, {
async fetch(url) {
return await E(httpClient).get(url); // Returns promise
}
});
The return guard expects a string, but gets a promise!
Solution:
const FetcherI = M.interface('Fetcher', {
// Correct: M.callWhen() for async method
fetch: M.callWhen(M.string()).returns(M.string())
});
Or if the method is truly synchronous, don't use async:
const FetcherI = M.interface('Fetcher', {
fetch: M.call(M.string()).returns(M.promise()) // Returns promise explicitly
});
const fetcher = makeExo('Fetcher', FetcherI, {
fetch(url) {
return E(httpClient).get(url); // Return promise, don't await
}
});
Problem:
const obj = Far('MyObject', {
doSomething(x) { /* ... */ }
});
// This works, but has no input validation
obj.doSomething('invalid'); // No guard to catch this
Far objects are remotable but don't validate inputs.
Solution:
Use makeExo when you need defensive behavior:
const obj = makeExo('MyObject', MyObjectI, {
doSomething(x) { /* ... */ }
});
// Now inputs are validated
obj.doSomething('invalid'); // throws if pattern doesn't match
Problem:
You can't pipeline to computed property names or conditional logic:
// This doesn't pipeline correctly
const methodName = await E(obj).getMethodName();
const result = E(obj)[methodName](); // Second call waits for first
Solution:
Design interfaces so common operations don't require computed dispatch:
// Better: explicit methods
const result = await E(obj).doCommonOperation();
Or use a dispatch method:
const result = await E(obj).dispatch(methodName, ...args);
Problem:
const config = harden({ timeout: 5000 });
config.timeout = 10000; // throws - object is frozen
Copyable data is frozen and can't be mutated.
Solution:
Create new data instead of mutating:
const oldConfig = harden({ timeout: 5000 });
const newConfig = harden({ ...oldConfig, timeout: 10000 });
You now understand the complete eventual send and receive stack. Here are resources for going deeper:
For detailed API reference:
CapTP: For real network communication between machines, see @endo/captp. CapTP implements the Cap'n Proto protocol for capability-based RPC.
Virtual and Durable Exos: The exos in this guide are heap-based and don't survive vat restarts. For high cardinality or upgrade-survivable exos, see @agoric/vat-data which provides:
defineVirtualExoClass - backed by virtual object storagedefineDurableExoClass - survives vat upgradesprepareExoClass - unified API for bothMarshal: For details on how passables are serialized for transmission, see @endo/marshal.
Stores: For persistent collections of passables and remotables, see @agoric/store.
The Agoric SDK contains numerous examples of message passing patterns in production smart contracts.
This completes the tour of message passing in Endo. These four packages form the foundation of safe distributed computing, enabling you to build capability-based systems that work seamlessly from local function calls to global network communications.