Home Blog Prototype
< back 2025/01/09 UTC — by Agwé Team

summary — We introduce a prototype for decentralized applications built around a namespace of predicates and an append-only log of their successful invocation. The predicates are validation rules for a set of events, which are recorded and from which the actual state of applications can be derived. We want to build computing systems which allow individuals to build entire (social-)networks -- similar to the Unix philosophy of many free and small applications that can compose. Here we have a simple implementation of such a system which runs locally in your browser. You define the entire namespace in JavaScript by writing functions that map any number of arguments to a bool. Each function is assigned an append-only log of the same name, which tracks when the function was successfully invoked from the frontend. The log tracks all the given parameters plus the logged-in user, a timestamp, and a unique event id. One can also define derived state, which can reduce arguments and the event stream to a single state element (like the health of an animal in a game). The derived functions do not affect the meaning of the underlying system, but instead are a way of providing authoritative interpretation of the events which have happened. The User Interface simply depends on the event stream and is generated through prompting Claude in this prototype.

A simple thought on networked application state.

Introduction

Our goal is to develop application protocols which empower the end user to decide the nature of the relationship with their devices. We think that the mechanics eroding users' agency today are largely economic. It is, for instance, theoretically possible for anyone to field their own social media service on the Internet, but it will be a very quiet place, since the network effect driven monopolies of platforms like Instagram cannot be overcome (and if they are overcome through a vastly better/different experience, the service is quickly acquired).

A platform that can break this cycle is such that different experiences do not have to compete for user lock-in, presumably because the artifacts owned by developers are sufficiently generic such that they can compose with one another to form the composite experience the user desires. We wish to give individual developers the tools to field a platform like Twitter or Instagram as an altruistic solo project. The closest analogy is the Unix philosophy of a thriving ecosystem of simple applications that compose. Except that the “applications” are networks (in the abstract sense of “social-network”) and we want to make them even simpler than Unix's C code.

Sticking with the nature analogies, we call the protocols we are developing substrates upon which the ecosystem can sustain itself.

A Local Prototype Substrate

This blog post is the description of a super simple substrate along with a prototype implementing it as a demo entirely locally in your computer. There is nothing revolutionary here, but it should serve as a nice illustration of the abstract ideas we've been thinking about. The rest of this post is organised as follows. The next section explains all the technical details of the prototype we are releasing today. After, we will highlight some example applications which are also available for you to try, and we will conclude with some future research ideas and outlook.

Technical Description

The overarching setup is to have a universal log-in handler which provides accounts that users can take with them to all applications. Here three users exist and you can just select which one is “logged-in” at a time to simulate different perspectives of the network: Alice, Bob, and Charlie.

The substrate we are showing here is based around a namespace of predicates to which any user can theoretically contribute, although for this prototype you can just define the entire namespace like a sort-of admin user. To illustrate, the “namespace of predicates” is mapping names like Messages.create to functions which take any number of arguments and return a boolean value, for instance Messages.create: (textContent: string) => textContent.length > 0. This would imply that creating a Message is valid if its content is non-empty. In this prototype you define the namespace with JavaScript like this:

function contactExists(contact){  
    const noAdds = query('Contacts.addContact', `$atp=@, contact=${contact}`).length;  
    const noRems = query('Contacts.removeContact', `$atp=@, other=${contact}`).length;  
    return noAdds > noRems;  
}

function likeExists(messageId){  
    const noAdds = query('Messages.like', `messageId=${messageId}`).length;  
    const noRems = query('Messages.removeLike', `messageId=${messageId}`).length;  
    return noAdds > noRems;  
}

const exported = {  
    Messages: {  
        create: (content = "type:string", to = "type:user") => content.length > 0,  
        like: (messageId = "type:uid") => !likeExists(messageId),  
        removeLike: (messageId = "type:uid") => likeExists(messageId),  
    }  
}  

This example comes with the odd convention that the types of the predicates' arguments are defined by default assigning a string specifying the type. Further, if an argument is default assigned with something not of the form type:*, it will be included in the predicate, but the user can't provide it from the outside. This is a way to set values in a trusted way and used extensively in the “Pets Game” example.

Beyond the namespace of predicates, the last piece of this substrate is an append-only log of all valid invocations of a predicate (which is called an event). For instance, if one wanted to create a message, they would sent from the frontend to the backend (substrate) to execute Messages.create('Hi Charlie!', 'Charlie') the backend would then check if the call is valid, and if it is would register an object like the following to the Messages.create append-only log:

{  
	$atp:  'Alice',  
	$uid: '39120451-f676-4fb7-84c5-c4402d7b83e4',  
	$timestamp: '1736042189',  
    content: 'Hi Charlie!',  
    to: 'Charlie',  
}  

The fields with a leading dollar-sign are added by the substrate, $atp is the logged-in user, $uid is the unique id of this event, and $timestamp is the timestamp. The other fields are the arguments passed to the predicate. Theoretically we'd maintain an absolute ordering of all events. In the prototype, you can see the append-only log in the “Backend” tab, where you can also reset it, which you'll need to, if you make changes to the type code.

There is also a tab to express “derived” state, which is a way of providing an explicit interpretation of the append-only log. This could all be expressed in the frontend, and any variations from the given interpretation of the log do not affect the overall robustness of the system, but since everyone needs to agree on the interpretation of the log, it is best provided explicitly. This is entirely philosophical: The state of the system is exactly the append-only log, if a user chooses to interpret the data differently (wrongly) then it only impacts them and their own misinterpretation. By defining the derived state explicitly, you apply the authority of the backend to extinguish any competing interpretations. An example from the “Pets Project”:

const exported = {  
    Pet: {  
        currentdHunger: (petUid = "type:uid") => {  
            const pet = query('Pet.initialize', `$uid=${petUid}`)[0];  
            if (pet === undefined) {  
                return null;  
            }

            const ticks = query('Pet.tick', `petUid=${petUid}`);  
            const count = numberOfTicksAway(petUid, ticks);

            return Math.min(1.0, pet.dHunger + count * improvementAmount)  
        },  
/*...*/  
    }  
};  

One writes a pure function of the append-only log to define the user-interface. Since writing UI code is annoying, in this prototype you can prompt Claude to create the frontend for you. It has the special ability to wrap React code in <React>...</React> tags and access to the useAgweQuery, and useAgweDerived hooks as-well-as the agweExecute function, to interact with the substrate -- each example has some UI code pre-generated. Be warned that it takes some faffing around with Claude to get the UI looking reasonable.

In the above longer code example we have seen how we can reflect on the append-only log to validate a predicate. The primary way to do this is through the query(question, constraint) function. The first argument is simply the name of the event stream to look up, the second is a string of key value pairs which have to all match exactly for an event to be returned. The symbol @ is special and means the current logged-in user. For instance, the query query('Contacts.addContact', $atp=@, contact=${contact}) matches all invocations of Contacts.addContact where the invocation was made by the current logged-in user ($atp=@) and the referenced contact is the contact variable (contact=${contact}).

There is no such thing as security and all users can see the entire event stream. This means that the frontend has to occasionally filter events in such a way that the displayed information makes sense. For instance, the frontend of the Contacts example filters out all events where the contact is not created by the logged-in user. Obviously, the given query function is very naive.

Structures

We would like to highlight and discuss some common structures which occur when using this substrate.

Lazy-Unrestricted

This is the kind of predicate like Messages.create, it does not depend on the append-only log and only performs a basic sanity check (Messages can't be empty). More interestingly this predicate is lazy in the sense that it does not check if the user the message is sent to exists (you may notice that this is a common pattern in blockchain Smart Contacts). This kind of structure exploits that creating messages to no one is fine, since no one will ever see them. This is only okay if users are referenced by collision-resistant unique ids, since otherwise writing fake messages can block future ones. This isn't the case in this prototype, but is trivial in any real system.

Both the direct messages and twitter clone examples use this type of structure for their messages/posts.

Flip-Flop Bool

This structure is a boolean which can only be changed from True to False and vice versa. It is useful when an action can only be done once, but is reversible.

It is implemented by having two events: One raising and the other lowering. The state of the boolean depends on the number of times the raising or lowering event has been created; if there are more raisings than lowerings, the boolean is True if there are more lowerings than raising the boolean is False. To force the “flip-flop” nature of the structure, the raising event is only valid if the boolean is False and the lowering event is only valid if the boolean is True. This sort-of structure is, for instance, implemented with Messages.like and Messages.removeLike through the function likeExists, which computes the state of the boolean.

Notice that this requires counting the number of events which have occurred, which is in general linear in time. It can be made constant times by tracking counts on the backend. In general, this substrate is limited by the time it takes to compute reductions over the event stream. Strictness in the nature of these reductions can be used to optimize execution time.

This structure is again used in the twitter clone and direct messages example.

Stateful Object

This structure is like an object with a set of fields that are mutated. It is constructed by having an initialize predicate which may have some “fields” as the default assigned parameters of the predicate. There are then events which modify the fields by referencing the id of the initialize event, for instance through a form like setValue(value, objectId) that sets the new value directly or events which modify the value incrementally. For the first case, the state is simply the value of the most recent setValue invocation. For the latter, the state must be derived by reducing over the entire event log.

This structure is extensively used in the digital pets example. Notably, the code for the derived state is quite obtuse. This could be improved by changing how the derived state is computed. For instance, one could define an object with some state and then write handler functions for each kind of event that exists in the namespace that pertains to this object. The event log would then be replayed, with each handler able to modify the state, until the final derived state has been reconstructed (This is like the baby version of how Urbit works).

Why is it called a “Substrate?”

Hopefully this discussion highlights how structures commonly seen when programming more directly in existing languages can be realised on this substrate. You may have noticed that a lot of these are actually quite convoluted. Setting up a backend system like we have here makes a lot of the traditionally hard parts of web apps (essentially anything related to persistent state) trivial, at the expense of expressing common programming concepts with quite some difficulty.

This trade-off is why we call such a system a “substrate,” in the loose sense that it is convenient for the computer to target, but not for the programmer. In the realm of offline computers, we'd call assembly a “substrate,” since it solves a lot of the tricky problems of hardware and can be written by humans, but is designed foremost with the computer in mind. Similarly to assembly, it is not difficult to imagine a high level language which is compiled to a namespace of predicates that also gives the affordances commonly expected by programmers.

Examples

Chat + Contacts

A super simple app that allows you to directly message other users, add them as contacts, and like messages. The full predicates for this example are below, and there is no derived state:
function contactExists(contact){
    const noAdds = query('Contacts.addContact', `$atp=@, contact=${contact}`).length;
    const noRems = query('Contacts.removeContact', `$atp=@, other=${contact}`).length;
    return noAdds > noRems;
}

function likeExists(messageId){
    const noAdds = query('Messages.like', `$atp=@, messageId=${messageId}`).length;
    const noRems = query('Messages.removeLike', `$atp=@, messageId=${messageId}`).length;
    return noAdds > noRems;
}


const exported = {
    Messages: {
        create: (content = "type:string", to = "type:user") => content.length > 0,
        like: (messageId = "type:uid") => !likeExists(messageId),
        removeLike: (messageId = "type:uid") => likeExists(messageId),
    },

    Contacts: {
        addContact: (contact = "type:user") => {return !contactExists(contact)},
        removeContact: (other = "type:user") => {return contactExists(other)},
    }
}

Both contactExists and likeExists are implementations of the "Flip-Flop Bool" described above. Other than that, there isn't much else going on.

If we view the "Messages" and "Contacts" names as separate apps, then this example also illustrates how applications can compose. For instance, we can ask Claude to highlight users in the Chat app, which are also in our contacts.

A fun thing to try is to use the buttons at the top of the chat window in the prototype to only expose the Messages or Contacts app to Claude for a while, and to then have it retroactively patch in support for the other app.

"Y" -- A Twitter Clone

This is a basic Twitter clone. The predicates are given below, and again there is no derived state.

function flipFlopBoolIsTrue(positive, negative, constraint){
    const noAdds = query(positive, constraint).length;
    const noRems = query(negative, constraint).length;
    return noAdds > noRems;
}

const exported = {
    Y: {
        createPost: (textContent = "type:string") => textContent.length > 0,
        
        createReply: (postOrReplyId = "type:uid", textContent = "type:string") => (
                query('Y.createReply',  `$uid=${postOrReplyId}`).length > 0  || 
                query('Y.createPost',  `$uid=${postOrReplyId}`).length > 0 ) && 
            textContent.length > 0,
        
        createLike: (postOrReplyId = "type:uid") => 
            !flipFlopBoolIsTrue('Y.createLike', 'Y.removeLike', `$atp=@, postOrReplyId=${postOrReplyId}`),
        
        removeLike: (postOrReplyId = "type:uid") =>  
            flipFlopBoolIsTrue('Y.createLike', 'Y.removeLike', `$atp=@, postOrReplyId=${postOrReplyId}`)
    }
};

The second image also shows the event stream, which brought about the example, and can be inspected in the "Backend" tab. This example is constructed much like the Chat + Contacts example previously, but the "Flip-Flop Bool" structure has been implemented more generically and most of the events are written in such a way that they can reference a "post" or a "reply".

Pets Game!

This is a cute Tamagotchi-esque pets game. It highlights how to deal with stateful objects which transform over their life-cycle. Each player can initialize a pet with a preferred name, which has some randomly generated stats. The pet can be fed and petted, with the improvement in the pet's condition per action depending on their randomly generated stats. A pet can be sent to visit another user's pet, and as time passes while they are visiting, their basic stats improve! The predicates are as follows.
function isMovedAway (petUid) {
    const noMovesAway = query('Pet.moveToFriend', `$atp=@, petUid=${petUid}`).length;
    const noMovesHome = query('Pet.moveHome', `$atp=@, petUid=${petUid}`).length;
    return noMovesAway > noMovesHome
}


const exported = {
    Pet: {
        initialize: (
            name = "type:string",
            hunger = 100,
            happiness = 100,
            
            dHunger = Math.random(),
            dHappiness = Math.random(),
        ) => name.length > 0,
        
        tick: (
            petUid = "type:uid",
            hungerChange = Math.random() * 33,
            happinessChange = Math.random() * 33,
        ) => query('Pet.initialize', `$atp=@, $uid=${petUid}`).length > 0,
        
        feed: (petUid = "type:uid") => query('Pet.initialize', `$atp=@, $uid=${petUid}`).length > 0,
        play: (petUid = "type:uid") => query('Pet.initialize', `$atp=@, $uid=${petUid}`).length > 0,

        moveToFriend: (petUid = "type:uid", otherUser = "type:user") => !isMovedAway(petUid),
        moveHome: (petUid = "type:uid") => isMovedAway(petUid),
    }
};

This example actually has derived state.

const improvementAmount = 0.05

function numberOfTicksAway(petUid, ticks) {
    const movesAway = query('Pet.moveToFriend', `petUid=${petUid}`);
    const movesHome = query('Pet.moveHome', `petUid=${petUid}`);
    
    const events = [
        ...ticks.map(tick => ({ 
            timestamp: tick.$timestamp, 
            type: 'tick' 
        })),
        ...movesAway.map(move => ({ 
            timestamp: move.$timestamp, 
            type: 'moveAway' 
        })),
        ...movesHome.map(move => ({ 
            timestamp: move.$timestamp, 
            type: 'moveHome' 
        }))
    ];

    // Sort events chronologically
    events.sort((a, b) => a.timestamp - b.timestamp);

    let tickCount = 0;
    let awayCount = 0;

    // Process events in chronological order
    events.forEach(event => {
        switch (event.type) {
            case 'moveAway':
                awayCount++;
                break;
            case 'moveHome':
                awayCount = Math.max(0, awayCount - 1); // Prevent negative counts
                break;
            case 'tick':
                // Count tick if pet is currently away
                if (awayCount > 0) {
                    tickCount++;
                }
                break;
        }
    });
    
    return tickCount;
}

const exported = {
    Pet: {
        currentdHunger: (petUid = "type:uid") => {
            const pet = query('Pet.initialize', `$uid=${petUid}`)[0];
            if (pet === undefined) {
                return null;
            }

            const ticks = query('Pet.tick', `petUid=${petUid}`);
            const count = numberOfTicksAway(petUid, ticks);

            return Math.min(1.0, pet.dHunger + count * improvementAmount)
        },

        currentdHappiness: (petUid = "type:uid") => {
            const pet = query('Pet.initialize', `$uid=${petUid}`)[0];
            if (pet === undefined) {
                return null;
            }
            
            const ticks = query('Pet.tick', `petUid=${petUid}`);
            const count = numberOfTicksAway(petUid, ticks);
            return Math.min(1.0, pet.dHappiness + count * improvementAmount)
        },
        
        currentFood: (petUid = "type:uid") => {
            const pet = query('Pet.initialize', `$uid=${petUid}`)[0];
            if (pet === undefined) {
                return null;
            }
            
            const ticks = query('Pet.tick', `petUid=${petUid}`);
            const feeds = query('Pet.feed', `petUid=${petUid}`);
            
            const foodLost = ticks.reduce((c, x) => c + x.hungerChange, 0);
            
            //const foodGained = feeds.length * pet.dHunger;
            let foodGained = 0.0;
            feeds.forEach(feed => {
                const ticks_ = ticks.filter(x => x.$timestamp < feed.$timestamp);
                const nAway = numberOfTicksAway(petUid, ticks_);
                foodGained += pet.dHunger + nAway * improvementAmount;
                
            })
          

            return pet.hunger - foodLost + foodGained;
        },
        
        currentHappiness: (petUid = "type:uid") => {
            const pet = query('Pet.initialize', `$uid=${petUid}`)[0];
            if (pet === undefined) {
                return null;
            }
            
            const ticks = query('Pet.tick', `petUid=${petUid}`);
            const plays = query('Pet.play', `petUid=${petUid}`);

            const happyLost = ticks.reduce((c, x) => c + x.happinessChange, 0);
            //const happyGained = plays.length * pet.dHappiness;
            let happyGained = 0.0;
            plays.forEach(play => {
                const ticks_ = ticks.filter(x => x.$timestamp < play.$timestamp);
                const nAway = numberOfTicksAway(petUid, ticks_);
                happyGained += pet.dHappiness + nAway * improvementAmount;
                
            })

            return pet.happiness - happyLost + happyGained;
        },
        
        currentLocation_returnsUserWherePetIs: (petUid = "type:uid") => {
            const pet = query('Pet.initialize', `$uid=${petUid}`)[0];
            if (pet === undefined) {
                return null;
            }
            
            const movesAway = query('Pet.moveToFriend', `petUid=${petUid}`);
            const movesHome = query('Pet.moveHome', `petUid=${petUid}`);
            
            if (movesAway.length > movesHome.length) {
                return movesAway.at(-1)['otherUser'];
            } else {
                return pet['$atp'];
            }
        }

    }
};

The derived state in general works by first figuring out how many ticks a pet has spent not at home. This is done by replaying the tick, moveAway, and moveHome events in their chronological order. How good the stats of the pet are can be computed fairly straightforwardly with this, which is done in currentdHunger and currentdHappiness. The actual food level and happiness level is then computed in currentHunger and currentHappiness. This requires some more event replaying shenanigans. It is also useful for the frontend to know at which user a pet currently is, so this state is also implemented. The odd name shows the need for some form of accompanying documentation, which can be shown to the chatbot which is generating the UI.

Conclusion

As mentioned above, none of this is revolutionary or difficult, and really more meant as an implementation that is "thinking out loud". The hope is to come up with programming paradigms that enable us to move away from the centralised internet, towards one that empowers users and respects people's self-determination. Again, we think that the nature of the current internet is largely due to economic effects and hope that carefully and caringly designed technology can harness such economic winds to achieve our stated goals instead of digging deeper moats for corporate interests.

The above text touches some ideas implicitly, which others have also recognised. One is the existence of a universal identity that is owned by the user. Such identity systems have been implemented as collision resistant addresses like those used on Bitcoin or PGP/GPG, domain based systems like Ethereum Name Service domains or traditional DNS, "digital land" based systems implemented through Non-fungible Tokens like Urbit's Azimuth, or even biometrics based ideas such as WorldCoin. Another is permissionless communication, which is implemented partially in decentralised databases/communication protocols such as Gun.js, CouchDB, or iroh.computer, and also obviously fully strictly in decentralised blockchains.

All these systems have complicated technological and social trade-offs, which need to be made carefully. At the end of the day, we think it is important to remember that the success of such a system relies on the presence of everyday people. Unlike some other projects, we do not wish to harbour a disdain for non-technical people, but instead wish to come up with metaphors which enables them to interact with their computer as profoundly as developers do. Therefore, our research efforts cannot strictly focus on the development of breathtakingly beautiful technology, but must also yield human/computer interfaces which are vastly more accessible than existing ones.