Skip to content

Reward automation

Governance lifecycle · Security architecture

Under CIP-104, an application provider earns traffic-based rewards as RewardCouponV2 coupons. When a Decentralized Party is the provider, each coupon names the party as provider and has no beneficiary. A coupon expires after about 36 hours. A Decentralized Party has no wallet automation. Its coupons therefore expire when nobody uses them in time.

Reward automation solves this in two parts:

  1. The members vote once to create a CouponReassignmentDelegation contract. The contract holds a fixed beneficiary split and a list of assigners.
  2. Each Decentralization Manager node runs a background loop. On each sweep, an assigner node assigns every unassigned coupon to the split. The assignment needs no vote.

After the assignment, each beneficiary holds its own coupons. Each beneficiary must mint its coupons before they expire. Decentralization Manager does not mint for the beneficiaries.

  • The Decentralized Party has an active GovernanceRules contract.
  • Every participant has the governance-rewards-automation-v1 DAR, uploaded and vetted. The repository publishes it as releases/v1/governance-rewards-automation-v1-0.1.0.dar. Package and contract operations describes the upload.
  • Every participant has the Splice packages that the DAR depends on, including splice-amulet and splice-api-reward-assignment-v1.
  • The participant of each assigner hosts the Decentralized Party. Otherwise the assigner cannot read the coupons.
  • Each assigner node has party credentials for the Decentralized Party. Authentication and operator roles describes the credentials.
  • Each beneficiary has a wallet or an agent that mints its coupons.

Create the delegation with a governance proposal. Complete your first governance action describes the propose, confirm and execute steps.

Step 1

In Parties, select the Decentralized Party. Then select New Proposal.

Step 2

In Proposal Type, under Rewards, select Setup Coupon Reassignment Delegation.

Step 3

  • DSO Party: the form fills in the DSO party of the network. Check the value.
  • Replaces Delegation: leave it empty for the first delegation.
  • Assigners: add each member party that can run the assignment with Add Assigner. Any one assigner is enough.
  • Beneficiary split: add each beneficiary with Add Beneficiary. Enter a whole-number Weight for each beneficiary.

Step 4

Select Submit Proposal. The members confirm the proposal in Approvals. A member then executes it.

The API equivalent is POST /governance/propose. GET /network-info returns the DSO party of the network as dso_party_id.

Terminal window
curl -X POST "https://<decentralization-manager-host>/governance/propose" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"party_id": "<decentralized-party-id>",
"rules_contract_id": "<governance-rules-contract-id>",
"proposal": {
"type": "setup_coupon_reassignment_delegation",
"dso": "<dso-party-id>",
"assigners": ["<member-party-id-1>", "<member-party-id-2>"],
"new_beneficiaries": [
{"beneficiary": "<beneficiary-party-id-1>", "percentage": "0.8"},
{"beneficiary": "<beneficiary-party-id-2>", "percentage": "0.2"}
],
"prior_delegation": null
}
}'
Field Meaning
dso The DSO party whose coupons the delegation can assign.
assigners The member parties that can run the assignment. Any one of them is enough.
new_beneficiaries The fixed split. Each entry names a party and its percentage as a decimal string.
prior_delegation The contract ID of the delegation to replace, or null for the first delegation.

Decentralization Manager checks these rules when you propose. The Daml action checks them again at execution.

  • The split has at least 1 and at most 20 beneficiaries.
  • Each percentage is greater than 0 and at most 1.
  • No beneficiary appears twice.
  • The percentages sum to exactly 1.0. The ledger compares exact decimals. Three shares of 0.3333333333 therefore fail. Balance the last share by hand, for example 0.3333333334.
  • The Decentralized Party keeps no share by default. To keep a share, add the Decentralized Party as a beneficiary.
  • The assigner list has at least one party, and no party appears twice.

The web form takes whole-number weights. It converts the weights to exact percentages and gives the rounding remainder to the largest weight. A confirmer can then reproduce the split from the weights.

  • Change the split or the assigners: propose Setup Coupon Reassignment Delegation again, and select the active delegation in Replaces Delegation. The execution archives the old delegation and creates the new one in one transaction.
  • Stop the automation: propose Revoke Coupon Reassignment Delegation, and select the delegation in Delegation Contract ID. The API type is revoke_coupon_reassignment_delegation with the field delegation.

Only a governance vote can replace or revoke a delegation. A proposal with an empty Replaces Delegation fails with HTTP 409 while a delegation is active. The same proposal fails with HTTP 503 if the node cannot read the active delegations.

Every Decentralization Manager node runs the reward loop. No variable turns it on or off. An active delegation on the ledger turns it on for a Decentralized Party.

  1. The loop wakes every 60 seconds, or more often if a reward interval is shorter. Each wake counts one heartbeat.
  2. Every DECPM_REWARD_AUTOMATION_INTERVAL_SECS, the loop sweeps each Decentralized Party that the node has party credentials for.
  3. The loop reads the active delegation of the party. It skips a party with no delegation.
  4. The loop checks that the delegation names the member party of this node as an assigner. If not, the node reads the backlog for the expiry gauge only. It also logs a warning.
  5. The loop reads the unassigned coupons of the party. It skips the coupons that expire within DECPM_REWARD_MIN_EXPIRY_MARGIN_SECS. It sorts the rest by expiry, soonest first.
  6. The loop assigns all of these coupons with Delegation_Assign transactions. Each transaction carries at most DECPM_REWARD_MAX_CREATES divided by the beneficiary count, rounded down, with a minimum of one coupon.

Between sweeps, the node reads the backlog every DECPM_REWARD_EXPIRY_READ_INTERVAL_SECS. This read refreshes the expiry gauge and assigns nothing.

The loop handles failures in two ways:

  • Contention or a network timeout: the node ends the sweep. The next sweep reads the ledger again. Several assigners often compete for the same coupons. This case is therefore normal.
  • Any other ledger rejection: the node sends the coupons of that transaction again, one coupon at a time. It skips each coupon that the ledger still rejects and logs it at the ERROR level. The next sweep tries the skipped coupons again.

If the participant has no rewards package, the automation stays off for that participant. It logs this state at the TRACE level only.

Any one assigner can reassign all unassigned coupons of the party at any time. The split in the delegation is the only possible destination. These checks keep the assignment on the correct contracts:

Check Where Result of a failed check
The delegation names this Decentralized Party in its decparty field. If several delegations are active, the newest one applies. Decentralization Manager, when it reads the delegation The node ignores the other delegations and logs a warning when several are active.
The member party of this node is in assigners. The comparison uses the full party ID, so a party with the same name or namespace does not match. Decentralization Manager The node reports the backlog and assigns nothing.
The delegation template is Governance.Rewards.CouponReassignmentDelegation:CouponReassignmentDelegation in the configured governance-rewards-automation-v1 package. The node reads the template from the ledger before each assignment. Decentralization Manager The node refuses the delegation and ends the sweep. The log shows refusing delegation from.
Each coupon comes from Splice.Amulet:RewardCouponV2. It names the Decentralized Party as provider and the delegation DSO as dso, and it has no beneficiary. Decentralization Manager, when it reads the coupons The node does not assign the coupon.
The assigner in the choice is in assigners. Delegation_Assign in Daml The ledger rejects the transaction.
Each coupon is a RewardCouponV2 with the DSO of the delegation. Delegation_Assign in Daml The ledger rejects the transaction.
The new beneficiaries are the split in the contract. The caller cannot supply them. Delegation_Assign in Daml Not applicable. The choice has no beneficiary argument.

These variables control the timing and the size of the assignments. The Configuration reference lists every DECPM_* variable.

Variable Default Purpose
DECPM_REWARD_AUTOMATION_INTERVAL_SECS 300 Seconds between sweeps. This value sets the cadence only. A sweep assigns every assignable coupon, so the interval changes latency, not throughput. A value of 0 runs as 1 with a warning.
DECPM_REWARD_EXPIRY_READ_INTERVAL_SECS 3600 Seconds between backlog reads that only refresh decman_reward_oldest_unassigned_expires_in_seconds. The gauge refreshes at the shorter of the two intervals. A value of 0 runs as 1 with a warning.
DECPM_REWARD_MAX_CREATES 100 Maximum output contracts that one Delegation_Assign creates. Lower it if assignments start to fail.
DECPM_REWARD_MIN_EXPIRY_MARGIN_SECS 120 Seconds that a coupon must have left before expiry to be assigned. The margin keeps a coupon from expiring during the submission.

The metrics in the next section use the metrics port DECPM_METRICS_PORT, 9464 by default. The metrics listener has no authentication and binds to DECPM_HOST.

  1. Check the delegation. The first entry is the delegation that the automation uses.

    Terminal window
    curl -s -H "Authorization: Bearer <token>" \
    "https://<decentralization-manager-host>/coupon-reassignment-delegation?party_id=<decentralized-party-id>"

    The response lists cid, dso, assigners and beneficiary_count for each active delegation. It normally holds one entry.

  2. Check the reward metrics on each assigner node.

    Terminal window
    curl -s "http://<node-address>:9464/metrics" | grep '^decman_reward_'
  3. Confirm that decman_reward_heartbeat_total increases and that decman_reward_sweep_total increases once per sweep interval.

  4. Confirm that decman_reward_coupons_assigned_total increases on at least one assigner node when the party earns coupons. The log shows reassigned coupon batch with the count.

  5. Check that decman_reward_oldest_unassigned_expires_in_seconds stays well above zero. The series disappears when the party has no unassigned coupons.

Each metric except the heartbeat has the label decparty.

Metric Meaning
decman_reward_heartbeat_total Wakes of the reward loop.
decman_reward_sweep_total Sweeps started for the party.
decman_reward_coupons_assigned_total Coupons that this node assigned.
decman_reward_coupons_skipped_total Coupons that the ledger rejected one at a time. The next sweep tries them again.
decman_reward_sweep_zero_assigned_total Sweeps that found assignable coupons and assigned none.
decman_reward_sweep_failed_total Sweeps that ended in an error.
decman_reward_expiry_read_total Backlog reads that only refreshed the expiry gauge.
decman_reward_credentials_unavailable_total Passes in which the token request failed for a party whose delegation names this node.
decman_reward_auth_unregistered_total Passes that found no authentication entry for a party whose delegation names this node.
decman_reward_oldest_unassigned_expires_in_seconds Seconds until the oldest unassigned coupon expires, from the last read.

Useful alerts include a heartbeat that does not increase for 15 minutes, and an expiry gauge below a margin that you choose. Monitoring and logging describes the metrics setup.

Symptom Cause and action
No decman_reward_sweep_total series for the party The node has no party credentials for the party. Configure the credentials.
Sweeps run, and the log shows the delegation does not name this node as an assigner The member party of this node is not an assigner. Replace the delegation with a corrected assigner list.
Sweeps run without errors, and no coupon is ever assigned No delegation is active, the DSO party in the delegation is wrong, or the participant has no rewards package. Check the delegation and the vetted packages.
decman_reward_sweep_zero_assigned_total increases The node found coupons and assigned none. Look for assign chunk failed or coupon rejected on its own in the log.
decman_reward_coupons_skipped_total increases The ledger rejected some coupons. The ERROR log lines name each coupon and the error ID.
decman_reward_credentials_unavailable_total or decman_reward_auth_unregistered_total increases The node cannot get a token for the party. Fix the party credentials.
The log shows refusing delegation from The active delegation does not come from the configured package. Replace it through governance.
The proposal fails with HTTP 409 A delegation is already active. Select it in Replaces Delegation.
  • The automation sees only the coupons that list the Decentralized Party as an observer. The DSO decides this when it creates a coupon. An invisible coupon looks the same as a coupon that the party never earned.
  • A split has at most 20 beneficiaries.
  • Canton cannot enforce one delegation for each party. If several delegations are active, the automation uses the newest one.
  • The automation does not mint. Each beneficiary must mint its coupons before they expire.

Watch the automation: Monitoring and logging.