Output
sf.substreams.sink.entity.v1.EntityChanges
Inputs
Package ref
agentrail-mandates@v0.1.0Run package
CLI
Run graph_out from the command line.
substreams run agentrail-mandates@v0.1.0 graph_out -e solana-devnetsubstreams auth or directly on thegraph.market (see docs).README
Indexes every transaction that touches the AgentRail program
(GcYqRmrRko3WbKNuGarDTbmRF1GcdeHzc3eV37gtM4Bj on devnet): mandates issued and revoked, permissions
added, and every execute_payment / verify attempt, including the ones the mandate refused.
A refused attempt is a landed, failed transaction. This package keeps it and maps the Anchor error
number to a block_reason:
| Error | Name | block_reason |
|---|---|---|
| 6006 | InstructionNotAllowed | NOT_PERMITTED |
| 6007 | PerTxLimitExceeded | OVER_BUDGET |
| 6008 | SpendLimitExceeded | OVER_BUDGET |
| 6001 | Expired | EXPIRED |
| 6000 | NotActive | REVOKED |
| other | FAILED (kept, never dropped) |
The program emits no events on purpose: reverted transactions cannot emit, and the reverts are the
product. Everything is decoded from instruction data (8-byte Anchor discriminator + Borsh args,
per target/idl/agentrail.json) and transaction meta (err, log_messages).
sf.solana.type.v1.Block ──▶ map_instructions ──▶ store_mandates (pda -> ens_node)
│ │
└──────▶ map_activity ◀┘ ──▶ graph_out
There is deliberately no blockFilter: solana-common's program_ids_without_votes index is
built from successful transactions, so a block whose only AgentRail transaction reverted would be
skipped and the refusal lost (verified live). Raw blocks cost more to process and keep every revert.
| Module | Output | What |
|---|---|---|
map_instructions | agentrail.v1.Activity | every AgentRail instruction in the block, successful or reverted |
store_mandates | store string | mandate PDA → ENS node, learned from create_mandate (the node sits at offset 80 of the account, unreadable from a block stream; the create argument carries it) |
map_activity | agentrail.v1.Activity | the shared-schema rows (indexer/schema.graphql), every action and permission joined to its ens_node |
graph_out | sf.substreams.sink.entity.v1.EntityChanges | the same rows as entity changes for a Substreams-powered subgraph |
ens_node is the join key shared with the Sepolia and Base Sepolia subgraphs: one query over
Mandate.ensNode spans all three chains.
map_instructions params: program:<base58> — decode another deployment of the program (also
change the blockFilter query, which is a static string).map_activity params: mandate:<pda>=<ens_node>;… — seed the join for mandates created before the
streamed range. The node is public on the ENS name, so this is data the operator already has.src/decode.rs for any Anchor program with a mandate-shaped instruction set; the error
parsing in src/errors.rs is program-independent.substreams auth # Graph Market token -> .substreams.env
. ./.substreams.env
cargo build --target wasm32-unknown-unknown --release && substreams pack substreams.yaml
substreams run -e devnet.sol.streamingfast.io:443 substreams.yaml map_activity -s 495587900 -t +12100
substreams gui -e devnet.sol.streamingfast.io:443 substreams.yaml map_activity -s 495587900
src/pb is generated by substreams protogen and committed, trimmed to the two protobuf
modules this crate uses (substreams build regenerates it with the manifest's exclude list; the
Solana block types come from the substreams-solana crate, never from local bindings).
cargo test runs the decoder against real devnet transactions in tests/fixtures (a full demo
run: create, permission, three payments of which two were refused, two verify of which one was
refused).
Modules
Output
sf.substreams.sink.entity.v1.EntityChanges
Inputs
Output
agentrail.v1.ActivityInputs
Output
agentrail.v1.ActivityInputs
Store value
string
Update policy
set
Inputs