Getting started guide

Autonomous agents allow to quickly create decentralized finance applications on a distributed ledger.

They operate based on code that is deployed on the ledger once and can never be changed. The code is open and is executed by all nodes on the network.

Anybody can activate an AA just by sending a transaction to its address. Here is an example of a simple AA:

["autonomous agent", {
    messages: [
        {
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{trigger.address}",
                        amount: "{trigger.output[[asset=base]] - 1000}"
                    }
                ]
            }
        }
    ]
}]

messages is a template of the response transaction that will be generated by the AA. It follows the structure of a regular Obyte transaction but its sections between curly braces {} contain code that makes the resulting transaction dependent on the triggering (activating) transaction. This is similar to how PHP code can be inserted into HTML to make the resulting page dependent on the request. The language autonomous agents are written in is called Oscript.

The above example of an AA just sends the received money less 1000 bytes back to the sender. trigger.address is the address of the sender who activated the AA. trigger.output allows to find the amounts in different currencies sent to the AA, trigger.output[[asset=base]] is the amount in the base asset -- bytes.

Another example that sells tokens for bytes at 1.5 tokens per byte:

["autonomous agent", {
    messages: [
        {
            app: "payment",
            payload: {
                // this is the token being sold
                asset: "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=",
                outputs: [
                    {
                        address: "{trigger.address}",
                        amount: "{ round(trigger.output[[asset=base]] * 1.5) }"
                    }
                ]
            }
        }
    ]
}]

This code needs to be deployed on the ledger before it can be used. Visit oscript.org, copy/paste the above code, and click "Deploy". You'll then get the address of your new autonomous agent. Anybody can now send money to this address to trigger execution of this AA's code.

Passing data to AA

AA's response can also depend on data it receives in the triggering transaction.

In Obyte, any transaction can contain one or more messages of different types. These types are called apps. The most common app is payment. At least one payment is mandatory in every transaction, it is necessary to at least pay fees. Another app is data. This type of message delivers an arbitrary json object that can contain any data:

{
    app: "data",
    payload: {
        withdrawal_amount: 25000,
        nested_array: [
            99,
            {another_field: 44}
        ],
        nested_object: {yet_another_field: "value2"}
    },
     ...
}

The object in payload is the data this message delivers.

When an AA receives a data message (along with a payment message, of course) in the triggering transaction, it can access the data through trigger.data and its action can depend on the data received:

["autonomous agent", {
    messages: [
        {
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{trigger.address}",
                        amount: "{trigger.data.withdrawal_amount}"
                    }
                ]
            }
        }
    ]
}]

The above AA tries to send trigger.data.withdrawal_amount bytes back to the sender. withdrawal_amount is a field in the data message of the triggering transaction:

{
    withdrawal_amount: 3000
}

AAs can be triggered with data by manually entering the key/value pairs to GUI wallet, prompting users to submit pre-filled Send screen, making the AA to trigger another AA or with the script from headless wallet.

Handling errors

In the above example, if the withdrawal_amount is greater than the balance that the AA has (including the amount received from the triggering transaction), the AA's response will fail. When an AA fails to handle a trigger transaction for any reason, it rolls back all changes and attempts to send back to sender all the received funds, in all assets, less the bounce fees. By default, the bounce fees are 10000 bytes for "base" asset and 0 for all other assets. This is also the minimum. The default can be overridden by adding a bounce_fees field in the autonomous agent definition:

["autonomous agent", {
    bounce_fees: {
        base: 20000,
        "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=": 100
    },
    messages: [
        {
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{trigger.address}",
                        amount: "{trigger.data.withdrawal_amount}"
                    }
                ]
            }
        }
    ]
}]

The above AA will charge 20000 bytes and 100 units of the asset "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=" if it has to bounce back a triggering transaction. Sending less than 20000 bytes to the AA will result in the AA silently eating the coins (they are not enough even for the bounce fees). When non-zero amount of asset "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=" is sent to the AA, it should also be at least 100, otherwise the AA eats the coins.

Sending data messages

As said above, payment is the most common message but other messages can be sent as well by any address, AA included.

Sending data feeds

AA can act as an oracle by sending a data feed:

["autonomous agent", {
    messages: [
        {
            app: "data_feed",
            payload: {
                "{trigger.data.feed_name}": "{trigger.data.feed_value}"
            }
        }
    ]
}]

This example shows that object keys can also be parameterized through Oscript, like object values.

The above AA doesn't have a payment message, it will be added automatically just to pay for the fees.

Sending data

An AA can also send any structured data:

["autonomous agent", {
    messages: [
        {
            app: "data",
            payload: {
                forwarded: 1,
                initial_data: "{trigger.data}"
            }
        },
        {
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY",
                        amount: "{trigger.output[[asset=base]] - 1000}"
                    }
                ]
            }
        }
    ]
}]

Unlike data_feed messages, data messages:

  • can have any structure with any nesting depth (data feeds are flat key-value pairs);

  • are not indexed for search;

  • can be used to pass data to other AAs.

The above AA forwards the received money (less 1000 bytes) to another address, which might be an AA. It also passes data, which includes the initially received data parameters. The value of trigger.data will be expanded into an object and added as value of initial_data field.

If the secondary recipient (JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY) happens to be an AA, its execution will start only after the execution of the current AA is finished. There is no reentrancy issue.

Sending texts

["autonomous agent", {
    messages: [
        {
            app: "text",
            payload: "{'Hello, ' || trigger.data.name || '!'}"
        }
    ]
}]

This AA will create a simple text message and store it on the DAG. || is the operator for string concatenation.

Defining new assets

["autonomous agent", {
    messages: [
        {
            app: "asset",
            payload: {
                cap: "{trigger.data.cap otherwise ''}",
                is_private: false,
                is_transferrable: true,
                auto_destroy: "{!!trigger.data.auto_destroy}",
                fixed_denominations: false,
                issued_by_definer_only: "{!!trigger.data.issued_by_definer_only}",
                cosigned_by_definer: false,
                spender_attested: false,
                attestors: [
                    "{trigger.data.attestor1 otherwise ''}",
                    "{trigger.data.attestor2 otherwise ''}",
                    "{trigger.data.attestor3 otherwise ''}",
                ]
            }
        }
    ]
}]

The above AA creates a definition of a new asset based on parameters passed in data.

Some syntax elements that we see here for the first time:

  • ! is a logical NOT operator;

  • otherwise is an operator that returns the first operand if it is truthy (anything except false, 0, and empty string) or the second operand otherwise;

  • when an object or array value evaluates to an empty string, the corresponding object/array element is removed. This means that if cap parameter is not passed then cap in payload evaluates to an empty string, therefore cap is removed from asset definition and an asset with infinite supply will be defined;

  • fields whose value is an empty array/object are also removed. Therefore, if none of the attestor1, attestor2, attestor3 fields was set, the attestors array becomes empty and will be excluded from the final definition.

Querying state

The AA's behavior can also depend on various state variables that describe the ledger as a whole.

Balance

["autonomous agent", {
	messages: [
		{
			app: "payment",
			payload: {
				asset: "base",
				outputs: [
					{
						address: "{trigger.address}",
						amount: "{ round(balance[base]/2) }"
					}
				]
			}
		}
	]
}]

Balance of other AAs (but not regular addresses) can also be queried:

["autonomous agent", {
    messages: [
        {
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{ (balance['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY'][base] < 1e6) ? 'JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY' : trigger.address }",
                        amount: "{ trigger.output[[asset=base]] - 1000 }"
                    }
                ]
            }
        }
    ]
}]

The above AA forwards any received payment (less 1000 bytes) to another AA JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY if its balance is less than 1,000,000 bytes, or returns to sender otherwise.

Data feeds

["autonomous agent", {
    messages: [
        {
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{(data_feed[[oracles='TKT4UESIKTTRALRRLWS4SENSTJX6ODCW', feed_name='BROOKLYNNETS_CHARLOTTEHORNETS_2019-07-21']] == 'CHARLOTTEHORNETS') ? '7XZSBB32I5XPIAVWUYCABKO67TZLHKZW' : 'FDZVVIOJZZVFAP6FLW6GMPDYYHI6W4JG'}"
                    }
                ]
            }
        }
    ]
}]

The above AA pays to 7XZSBB32I5XPIAVWUYCABKO67TZLHKZW if Charlotte Hornets won and to FDZVVIOJZZVFAP6FLW6GMPDYYHI6W4JG otherwise TKT4UESIKTTRALRRLWS4SENSTJX6ODCW is the address of a sports oracle that posts the results of sports events.

Before the data feed is posted, any attempt to evaluate it will fail the AA and bounce the triggering transaction.

Note that the amount field in the output is omitted, which means that the entire AA balance will be paid out.

Attestations

["autonomous agent", {
    messages: [
        {
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{trigger.address}",
                        amount: "{ (attestation[[attestors='JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725', address=trigger.address]].reputation >= 50) ? 50000 : 0 }"
                    }
                ]
            }
        }
    ]
}]

The above AA pays to the triggering user 50000 bytes if his address is attested by Steem attestation bot JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725 and his Steem reputation is at least 50. Otherwise, it "pays" 0 bytes. Trying to pay 0 amount results in the output being removed. Since it is the only output in this case and it is removed, the resulting response transaction would produce no effect, therefore it is not created at all.

Variables

State variables

An autonomous agent can have its own state saved between invocations. Use var to access and assign state variables:

["autonomous agent", {
    messages: [
        {
            if: "{ var['previous_address'] }",
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{ var['previous_address'] }",
                        amount: "{ trigger.output[[asset=base]] - 1000 }"
                    }
                ]
            }
        },
        {
            app: "state",
            state: `{
                var['previous_address'] = trigger.address;
            }`
        }
    ]
}]

The above AA saves the address that sent the previous triggering transaction, waits for the next invocation, and sends the received money less 1000 bytes to the previous user.

Assigning state variables is only allowed in the special message with app set to state. This message must have a single other field called state which is a set of Oscript statements. These statements are allowed to assign state variables. The state message is not included in the final response transaction, it can only affect state by modifying the state variables.

When an uninitialized var is accessed, it evaluates to false.

The above example has an if field in the first message. When var['previous_address'] is not initialized yet (first call), the if oscript evaluates to false and the entire first message is excluded from the transaction.

State variables of other AAs can also be read, but not assigned:

var['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY']['var_name']

Local constants

Use local constants to save intermediate results during script execution.

["autonomous agent", {
    messages: [
        {
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{ trigger.address }",
                        amount: `{
                            $amount1 = trigger.output[[asset=base]] - 5000;
                            $amount2 = round(balance[base] * 0.9);
                            max($amount1, $amount2)
                        }`
                    }
                ]
            }
        }
    ]
}]

Local constant names are prefixed with $. Unlike state variables, they are not saved between invocations.

To make it easier to argue about a local constant's value, there is one restriction that is not common in other languages: single-assignment rule. A local constant can be assigned a value only once and that value remains constant until the end of the script. An attempt to re-assign a value will fail the script and make the AA bounce.

Conditional sections

As we've seen in the State variables example before, some parts of the response transaction generated by the AA can be excluded by adding an if field. If the Oscript in the if evaluates to true or any truthy value (anything except false, 0, and empty string), the enclosing object is included in the final response, otherwise, it is excluded.

The if field itself is of course always removed from the final response.

Initialization code

Along with if, any object can have an init field that includes initialization code:

["autonomous agent", {
    messages: [
        {
            if: "{ trigger.data.pay }",
            init: `{
                $amount1 = trigger.output[[asset=base]] - 5000;
                $amount2 = round(balance[base] * 0.9);
            }`,
            app: "payment",
            payload: {
                asset: "base",
                outputs: [
                    {
                        address: "{ trigger.address }",
                        amount: "{max($amount1, $amount2)}"
                    }
                ]
            }
        }
    ]
}]

The init code contains only statements, it does not return any value unlike most other Oscripts. The other exception is state message described in State variables above, it is also statements-only. All other Oscripts can contain 0 or more statements but must end with an expression (without a ;) whose value becomes the value of the Oscript.

Init code is evaluated immediately after if (if it is present and evaluated to true of course) and before everything else. It is often used to set local constants that are used later.

Local constants in if and init

Local constants that are set in if and init are also available in all other Oscripts of the current and nested objects. In the above example, $amount1 and $amount2 were set in init of the message object, and they are used in payload/outputs/output-0/amount of this message.

Local constants set in if are also available in the corresponding init.

Statements-only vs return-value Oscripts

As said above, there are two types of Oscripts: those that contain only statements and those that return a value.

There are only two statements-only Oscripts: init and state script. Here is an example of an init script that we've seen before:

{
    init: `{
        $amount1 = trigger.output[[asset=base]] - 5000;
        $amount2 = round(balance[base] * 0.9);
    }`,
     ...
}

All other Oscripts return a value. They either contain a single expression:

{
    address: "{ trigger.address }",
     ...
}

or a set of statements and an expression at the end:

{
    amount: `{
        $amount1 = trigger.output[[asset=base]] - 5000;
        $amount2 = round(balance[base] * 0.9);
        max($amount1, $amount2)
    }`,
     ...
}

The value of the last expression is the value of the Oscript.

Cases

When you need to route the code execution along several mutually exclusive paths depending on input and environment parameters, use cases:

["autonomous agent", {
    messages: {
        cases: [
            {
                if: "{trigger.data.define}",
                messages: [
                    // first version of messages
                    ....
                ]
            },
            {
                if: "{trigger.data.issue}",
                init: "{$amount = trigger.output[[asset=base]];}",
                messages: [
                    // second version of messages
                    ....
                ]
            },
            {
                messages: [
                    // default version of messages
                    ....
                ]
            }
        ]
    }
}]

The regular messages array is replaced with an object with a single element called cases. cases is an array that lists several mutually exclusive alternatives. Every alternative except the last must have an if field. The first alternative whose if evaluates to true defines the messages that will take the place of the higher-level messages field.

The conditions in the ifs may not be mutually exclusive but the alternative that is listed earlier in the list takes precedence. Only one alternative is always taken.

In the above example, if trigger.data has no define field but has a truthy issue field, the 2nd alternative is selected, and the original object folds into:

["autonomous agent", {
    messages: [
        // second version of messages
        ....
    ]
}]

Cases can be nested. Everything said above about if, init, and local constants, applies to cases as well.

if else

The above examples showed how execution can be branched along different parts of json template.

Oscript code itself can be branched as well by using if/else:

if (var['started']){
    $amount = balance[base];
    $current_winner = var['current_winner'];
}
else{
    $amount = trigger.output[[asset=base]];
    $current_winner = trigger.address;
}

return

If you want to interrupt a script's execution and return from it, with or without a value, use return.

Here is an example of return in a return-value Oscript, it must return a value:

$amount = trigger.output[[asset=base]];
if ($amount > 1e6)
    return $amount;
// here comes some other code that will not be executed if $amount > 1e6

A return in a statements-only Oscript, such as init, doesn't return any value, it just interrupts the script:

$amount = trigger.output[[asset=base]];
if ($amount > 1e6)
    return;
// here comes some other code that will not be executed if $amount > 1e6

bounce/require

If you find an error condition and want to prematurely stop the AA execution and bounce, use bounce:

$maturity = var['maturity_timestamp'];
if (timestamp < $maturity) {
    bounce("too early");
}

or require:

$maturity = var['maturity_timestamp'];
require(timestamp >= $maturity, "too early");

In this example, timestamp is roughly the current timestamp (number of seconds since Jan 1, 1970 00:00:00 UTC).

All applied changes will be rolled back and any received coins will be bounced back to the sender less the bounce fees.

_________________

This was a quick introduction to programming of autonomous agents. Read the full language reference to learn more, and enjoy your journey through decentralized finance and beyond!

Last updated