Requesting Offchain Permits

After a managed offchain risk policy protects a function, the function rejects calls that do not have a matching permit. Your application must request an evaluation, wait for the permit to be stored onchain, and then submit the protected transaction.

This guide uses an ERC-20 transfer(address,uint256) as the example. For policy and protection setup, see Managing Offchain Policies (MVP).

Evaluation flow

  1. Your application describes the intended transaction to the ACE Evaluation API.
  2. ACE triggers the managed CRE workflow for your organization.
  3. The workflow extracts the configured addresses and screens them with TRM Wallet Screening.
  4. If the risk rules reject any address, the evaluation becomes rejected and no permit is created.
  5. If the risk rules pass, the workflow writes a permit to the CADV contract through the Keystone Forwarder.
  6. After ACE observes the onchain PermitStored event, the evaluation becomes ready.
  7. Your application submits the protected transaction with the same caller, target, function, and parameter values.
  8. The policy engine finds and consumes the permit. The permit cannot authorize another transaction.

Prerequisites

Before requesting an evaluation, verify that:

  • The offchain policy has deployment_status: active.
  • Its protection for the target function has status: active.
  • You know the target contract address and chain selector.
  • You know the wallet that will submit the onchain transaction. It must be the same address as caller_address.
  • You know the ordered extractor outputs configured on the protection. Your permit_parameters must use that same order.

Evaluation API

The production Evaluation API base URL is:

https://ace.api.chain.link/v1/evaluation

It uses the same ACE API key as the Coordinator API:

Authorization: Apikey <API_KEY>

Construct the evaluation request

Start an evaluation with POST /evaluate:

{
  "caller_address": "0x1111111111111111111111111111111111111111",
  "subject": "0x2222222222222222222222222222222222222222",
  "function_signature": "transfer(address,uint256)",
  "parameters": {
    "to": "0x3333333333333333333333333333333333333333",
    "amount": "100"
  },
  "permit_parameters": [
    "0x0000000000000000000000001111111111111111111111111111111111111111",
    "0x0000000000000000000000003333333333333333333333333333333333333333",
    "0x0000000000000000000000000000000000000000000000000000000000000064"
  ],
  "chain_selector": "<CHAIN_SELECTOR>",
  "unique_evaluation_id": "transfer-018f6b3e-7c42-7a1f-a8ed-5ecf90c03b30"
}
Field
Description
caller_addressWallet that will submit the protected transaction.
subjectAddress of the protected target contract.
function_signatureCanonical function signature, such as transfer(address,uint256). Do not send the four-byte selector.
parametersStructured representation of the function arguments. ACE stores it with the evaluation as contextual data.
permit_parametersOrdered ABI-encoded values used for address screening and exact onchain permit matching.
chain_selectorChain where the target, policy engine, protection, and CADV are deployed.
unique_evaluation_idClient-generated identifier unique to this transaction intent. ACE uses it to derive the permit ID.

Encode permit parameters

Each permit_parameters item is a 0x-prefixed, 32-byte ABI word. The items must have the same order as the extractor_output_ids on the protection.

For transfer(address,uint256), the ERC20TransferExtractor produces:

[from, to, amount]

Therefore, encode:

  1. from: the transaction caller, as an ABI address.
  2. to: the transfer recipient, as an ABI address.
  3. amount: the transfer amount, as an ABI uint256.

Use a standard ABI library rather than concatenating untrusted values manually. For example, with ethers v6:

import { AbiCoder } from "ethers"

const abiCoder = AbiCoder.defaultAbiCoder()

const permitParameters = [
  abiCoder.encode(["address"], [callerAddress]),
  abiCoder.encode(["address"], [recipientAddress]),
  abiCoder.encode(["uint256"], [amount]),
]

Choose a unique evaluation ID

unique_evaluation_id is scoped to your ACE organization. ACE combines it with the organization ID to derive a deterministic permit_id.

Retrying with the same unique_evaluation_id is idempotent: ACE returns the existing evaluation instead of triggering another workflow execution. Never reuse an ID for a different caller, target, function, or set of parameters.

Use a UUID or another collision-resistant identifier generated by your backend. Store it with the transaction intent so you can safely recover from a lost HTTP response.

Start the evaluation

curl -X POST https://ace.api.chain.link/v1/evaluation/evaluate \
  -H "Authorization: Apikey <API_KEY>" \
  -H "Content-Type: application/json" \
  -d @evaluation.json

The response contains the deterministic permit ID and initial status:

{
  "permit_id": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "evaluating"
}

The response does not mean that the transaction is approved. Wait until the evaluation becomes ready.

Poll the evaluation

Retrieve the evaluation using the returned permit ID:

curl \
  https://ace.api.chain.link/v1/evaluation/evaluate/<PERMIT_ID> \
  -H "Authorization: Apikey <API_KEY>"

Polling every five seconds is a reasonable default. Stop when the evaluation reaches a terminal status.

StatusTerminalMeaning
evaluatingNoThe workflow is screening the configured addresses.
approvingNoTRM checks passed and the workflow is publishing the permit onchain.
readyYesThe permit was stored onchain. The protected transaction can now be submitted.
rejectedYesAt least one configured risk rule rejected the evaluation. No permit was created.
errorYesThe evaluation or onchain permit publication failed. No usable permit is available.

For rejected and error, the response can include a reason. workflow_execution_id identifies the CRE execution when available. Because permits do not expire in the current release, expires_at is normally null.

{
  "permit_id": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "ready",
  "reason": null,
  "workflow_execution_id": "<WORKFLOW_EXECUTION_ID>",
  "expires_at": null
}

Retry an evaluation

The CRE HTTP trigger allows one new execution per workflow every 60 seconds. Polling an existing evaluation does not trigger the workflow and is not subject to that trigger rate.

  • If the initial HTTP response is lost or ambiguous, retry POST /evaluate with the same unique_evaluation_id. ACE returns the existing evaluation if it was created.
  • If an evaluation reaches rejected, changing the identifier alone does not change the policy decision. Review the risk result or transaction intent.
  • If an evaluation reaches error and the underlying issue is resolved, wait at least 60 seconds and submit a new evaluation with a new unique_evaluation_id.

See CRE Service Quotas for current workflow limits.

Submit the protected transaction

Submit the transaction only after the evaluation becomes ready. The sender must be caller_address, and the target function must receive values that produce the same extracted parameters as permit_parameters.

No permit bytes are added to the transaction. The CADV already stores the permit and looks it up from the action's caller, target, selector, and extracted parameters.

After the protected call succeeds, the CADV increments the permit's usage counter. Managed risk policy permits have maxUses = 1, so another transaction with the same intent requires a new evaluation and permit.

Troubleshooting

Evaluation is rejected

  • At least one address met or exceeded the global risk_threshold.
  • A TRM risk indicator met or exceeded a configured category threshold.
  • TRM returned UNKNOWN and block_unknown is enabled.

Review the response reason and the policy configuration. Do not retry a rejected intent without understanding why it was rejected.

Evaluation returns an error

  • The Vault DON secret identifier does not match secret_name.
  • The TRM credential was not encoded as <API_KEY>:<API_KEY> before Base64 encoding.
  • TRM or the CRE confidential HTTP request failed.
  • The selected chain does not have an active CADV for the policy.
  • The workflow could not write the permit onchain.

If a TRM HTTP error should allow the action, review the policy's fail_mode. Use OPEN only after assessing the compliance impact.

Evaluation is ready but the transaction reverts

  • The transaction sender differs from caller_address.
  • The target address or function differs from the evaluation.
  • The eventual transaction produces different extractor values than permit_parameters.
  • The protection's extractor outputs are missing or ordered differently.
  • The permit has already been consumed.
  • Another policy in the target function's policy chain rejected the call.

Get the latest Chainlink content straight to your inbox.