Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

dedalo.toml

The funding policy. It lives at the repository root, it is committed, and it is reviewed like any other change — because it decides what people are paid.

Every table is validated on load, and unknown keys are rejected rather than ignored. A typo in a key name would otherwise silently fall back to a default, and a default fee schedule is not a thing to arrive at by accident.

dedalo init writes a commented template. Below is every key it can contain.


[project]

[project]
name = "my-project"
repository = "https://github.com/me/my-project"
open_collective = "my-project"
KeyRequiredDefaultMeaning
nameyes—Project name, used in plans and reports. Part of the plan id.
repositoryno—Canonical repository URL.
open_collectiveno—Open Collective slug this project self-funds through.

[git]

[git]
branch = "main"
lands_as = "merges"
ignore_subjects = ["chore(release)", "Merge branch"]
ignore_emails = ["noreply@github.com", "actions@github.com"]
KeyDefaultMeaning
branch"main"Changes landing on this branch are what earn a payout.
lands_as"merges"What a landed change looks like here: "merges" (a merge commit) or "commits" (every commit on the branch’s first-parent line).
ignore_subjects[]Changes whose subject starts with one of these are skipped entirely.
ignore_emails["noreply@github.com", "actions@github.com"]Emails that never receive a payout, however much they commit.

lands_as, and why it is not detected for you

GitHub offers three ways to land a pull request and only one of them makes a merge commit:

Merge methodMerge commitlands_as = "merges" sees it
Create a merge commityesyes
Squash and mergeno — one ordinary commitnothing
Rebase and mergeno — commits replayednothing

Squash-and-merge is the default many projects pick, and on such a repository lands_as = "merges" reports zero pending work on a history full of merged pull requests. dedalo scan says so explicitly when it finds no merge commit anywhere on the branch, rather than printing an empty table and letting you conclude there is nothing to pay for.

Setting lands_as = "commits" pays for every commit on the branch’s first-parent line. A merge commit there still counts, and still brings in the work on its second parent, so a history mixing both is not counted twice.

The trade is worth stating: "commits" pays for a direct push as readily as for a pull request. On a branch that requires pull requests those are the same thing. On one that does not, anybody with write access can write themselves a payout — so a repository using "commits" should protect the branch it pays for.

This is a setting rather than a detection because it decides what every contributor receives. A tool that changed its mind about that between runs, because a history happened to grow its first merge commit, would be worse than one that asks.

ignore_subjects matches on prefix; ignore_emails matches exactly. No globbing, no regular expressions — a pattern language here is a place for a subtle mistake to hide, and what it decides is who gets paid.

Note — the default ignore_emails covers GitHub’s own noreply and Actions addresses. If your CI commits under a different address, add it. A bot with a wallet is a way for a round to leak.

[attribution]

[attribution]
base_points = 100
points_per_insertion = 1.0
points_per_deletion = 0.5
max_points_per_merge = 5000
credit_merger = false
split_with_co_authors = true
KeyDefaultMeaning
base_points100Flat score every merged pull request earns, regardless of size.
points_per_insertion1.0Score per added line.
points_per_deletion0.5Score per removed line. Deleting code is work too.
max_points_per_merge5000Ceiling per merge, so one vendored dependency cannot drain a round.
credit_mergerfalseAlso credit whoever pressed merge, on top of the commit authors.
split_with_co_authorstrueShare a commit’s score with its Co-authored-by: trailers.

The two per-line values are decimals in the file because that is how people think about them, and they are converted to integer milli-points once, at load. Nothing downstream sees a float. See Attribution.

Money — changing anything in this table changes what people are paid, so it changes the plan id too. Under the release policy a change to attribution defaults in Dedalo itself is a breaking change even when it compiles.

[asset]

[asset]
symbol = "USDC"
decimals = 6
chain = "devnet"
contract = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
KeyRequiredMeaning
symbolyesDisplay symbol.
decimalsyesDecimal places the token uses on chain. Every amount you type is converted to base units with this.
chainyesChain identifier, e.g. base. Cross-checked against address formats.
contractnoToken contract address. Omit for the chain’s native coin.

Careful — decimals is not cosmetic. It converts --amount 1000 into base units. Getting it wrong by one scales every round by ten, in whichever direction is worse.

[fees]

[fees]
protocol_bps = 250     # 2.5% → the network's Open Collective
treasury_bps = 1500    # 15%  → this project's reserve
                       # 82.5% → contributors
KeyDefaultMeaning
protocol_bps250Share routed to the Open Collective wallet that funds the network.
treasury_bps1500Share retained by the project for future rounds, audits, infrastructure.

Basis points: 10,000 = 100%. A schedule where the two reach 10,000 is rejected, because contributors would receive nothing and that is never what somebody meant to configure.

Fees are taken off the top, protocol first, and they round down — the remainder stays with contributors. Both properties are proved over every schedule that validates. See Money.

[wallets]

[wallets]
source = "0x…"           # funds each round is paid out of
treasury = "0x…"         # this project's own reserve
open_collective = "0x…"  # the network's wallet, receiving protocol_bps

All three are required and all three are validated on load. dedalo init writes the zero address as a placeholder, and settlement refuses to send to it.

Careful — a placeholder is refused. A wrong real address is refused by nothing. Confirm each of these out of band before a round moves money, and read how strong the checksum is before deciding that a valid address is a correct one.

[settlement]

[settlement]
backend = "dry-run"
# rpc_url = "https://…"
# cluster = "devnet"
# contract = "0x…"
KeyDefaultMeaning
backend"dry-run"dry-run computes and verifies without spending. evm validates and builds the call, then refuses to sign.
rpc_url—JSON-RPC endpoint of the chain.
cluster—EIP-155 chain id, checked against the endpoint.
contract—Claim contract a round is deposited into.

There is no key here, and there must never be one. settlement.signer_env, which named an environment variable holding a signing key, was removed on purpose. Dedalo does not sign; dedalo propose prints transactions for a multisig. See Funding from a multisig.

[[identities]]

[[identities]]
handle = "ada"
wallet = "0xAdA0000000000000000000000000000000000000"
emails = ["ada@example.com", "ada@work.example"]
KeyMeaning
handleLabel used in reports. Usually a GitHub username; nothing checks it.
walletDestination address, validated and checksummed on load.
emailsEvery git author email this person commits under.

Repeat the table for each contributor. Manage it with dedalo identity rather than by hand — the command validates the address and reports how much that validation is worth.

One handle, one wallet, many emails: that is what makes one wallet, one transfer true.


A complete example

[project]
name = "my-project"
repository = "https://github.com/me/my-project"
open_collective = "my-project"

[git]
branch = "main"
lands_as = "commits"
ignore_subjects = ["chore(release)"]
ignore_emails = ["noreply@github.com", "actions@github.com"]

[attribution]
base_points = 100
points_per_insertion = 1.0
points_per_deletion = 0.5
max_points_per_merge = 5000
credit_merger = false
split_with_co_authors = true

[asset]
symbol = "USDC"
decimals = 6
chain = "devnet"
contract = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"

[fees]
protocol_bps = 250
treasury_bps = 1500

[wallets]
source = "So11111111111111111111111111111111111111112"
treasury = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
open_collective = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"

[settlement]
backend = "dry-run"

[[identities]]
handle = "ada"
wallet = "0xAdA0000000000000000000000000000000000000"
emails = ["ada@example.com"]