rubric/apa-v1)Status: Draft Version: 1.0.0-draft.2 Date: 2026-07-31 Editor: Echelon Intelligence Group LLC (Rubric Protocol) Reference implementation: @rubric-protocol/sdk v1.5.0, attestBeforeSpend()
Agent payment infrastructure has outpaced agent accountability infrastructure. HTTP 402 settlement, agentic wallets, and network-level mandate schemes now let software initiate payment autonomously. Identity layers answer *who the agent is*; mandate layers answer *what it was permitted to do*. Neither produces a durable, independently verifiable record of *what the agent actually did and on what basis*.
This profile specifies such a record. It defines two attestation schemas, the binding between them, the canonicalization and commitment scheme used to make them verifiable by a third party, and the gate semantics that make the record a precondition of the payment rather than a log written after it.
This profile is rail-agnostic. It applies equally to x402/HTTP 402 settlement, custodial or TEE-backed agentic wallets, and card-network mandate flows, and it does not require the payment rail to be aware of it.
The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as described in RFC 2119.
rubric/apa-v1 object attested before a payment.rubric/apa-receipt-v1 object attested after settlement is attempted.A conformant implementation produces a three-link chain:
A1 mandate credential (out of scope; referenced by mandateRef) | A2 decision record rubric/apa-v1 | leafType AGENT_SPEND_DECISION | attested BEFORE the payment executes | A3 receipt record rubric/apa-receipt-v1 leafType AGENT_SPEND_RECEIPT references A2 by attestation ID
A2 MUST be acknowledged by the issuing node before the payment function is invoked. A3 MUST carry the attestation ID assigned to A2.
| Field | Type | Req. | Description |
|---|---|---|---|
| schema | string | MUST | Literal rubric/apa-v1 |
| agentId | string | MUST | Stable identifier of the acting agent |
| mandateRef | string | SHOULD | Identifier of the authorizing mandate |
| intent | string | MUST | Human-readable statement of purpose |
| rail | string | MUST | Settlement rail (x402, card, wallet) |
| payee | string | SHOULD | Destination identifier |
| amount | string | SHOULD | Decimal string; not a float |
| currency | string | SHOULD | Asset or currency code |
| toolName | string | SHOULD | Name of the invoked payment tool |
| paramsHash | string | MUST | SHA3-256 hex of canonicalized call parameters |
| params | object | SHOULD | The call parameters themselves |
| timestamp | string | MUST | RFC 3339 UTC, generated client-side |
Implementations SHOULD also include the parameter object itself as params. The whole decision record is encrypted at rest under a per-attestation key that the issuing node returns to the caller and does not retain, so including params makes the inputs recoverable by the key holder without exposing them to the node operator or to anyone reading the ledger. paramsHash remains the public commitment, so a party can prove which inputs were used without disclosing them. Implementations that omit params can prove the inputs only if the principal separately retained them.
Parameters that must never be recoverable — raw secrets, card numbers, credentials — MUST NOT be placed in params. Commit to them via paramsHash and hold them elsewhere.
Undefined-valued properties MUST be omitted at construction. Numeric monetary values MUST be represented as strings to avoid IEEE 754 round-tripping ambiguity.
A decision record MAY be signed by the submitting client before submission. Where it is, the issuing node verifies the signature and, only if verification succeeds, carries the authoring public key into the anchored leaf as a client_attestation member.
The property this establishes is distinct from everything else in this profile. Absent a client signature, a decision record evidences what the issuing node received, and that node could in principle have fabricated it. With one, the record evidences what a holder of a specific private key submitted, and no node - including the issuing node - can produce a record attributable to that key.
Submission format. The client signs the canonicalized record with ML-DSA-65 and appends four members to it:
clientSignature hex-encoded ML-DSA-65 signature clientPublicKey hex-encoded public key clientSignedAt RFC 3339 UTC signatureAlgorithm "ML-DSA-65"
The signature MUST cover the record as it stood BEFORE those four members were added. A verifier strips them, canonicalizes, and checks.
Canonicalization for client signatures. This is the one place in this profile where section 6 does not apply. The reference SDK signs over a plain recursive key sort that neither omits undefined members nor applies RFC 8785 number formatting. An implementation verifying a client signature MUST reproduce the client's rule rather than the node's. The two agree on records built per section 4, where every value is a string, but they are not the same function and MUST NOT be substituted for one another. Verifying with the wrong rule rejects valid signatures.
Leaf carriage. Only a verified signature enters the leaf. An unverified one is recorded for diagnosis and MUST NOT appear in anchored evidence: a public key in an anchored record implies authorship, and implying authorship that did not check out is worse than claiming none.
| Field | Type | Req. | Description |
|---|---|---|---|
| schema | string | MUST | Literal rubric/apa-receipt-v1 |
| decisionAttestationId | string | MUST | Attestation ID of the corresponding A2 |
| txRef | string | SHOULD | Rail-native transaction reference |
| status | string | MUST | settled or failed |
| timestamp | string | MUST | RFC 3339 UTC |
Both record types MUST be canonicalized per RFC 8785 (JSON Canonicalization Scheme) with the clarifications in Rubric Verify Spec v1.0.0-rc2 section 6.1:
Canonicalization is normative. Client and issuing node MUST use equivalent implementations; the reference SDK vendors the identical module used by the node rather than reimplementing it, and implementers are strongly advised to do the same. Independent reimplementation is the primary source of silent verification failure.
The issuing node does not retain record plaintext. It stores a hiding and binding commitment:
salt = SHA-256( payloadKeyHex + ":rubric-commit-v1" ) commitment = SHA-256( salt + canonicalize(record) )
The payloadKey is generated per attestation, returned to the client in the attest response, and never retained by Rubric. It is unrecoverable if lost.
Verification procedure. A third party given (a) the record, (b) the payloadKey, and (c) the anchored commitment recomputes the salt, recomputes the commitment over the canonicalized record, and compares. A match proves the anchored commitment is to that exact record.
The salt is one-way derived from the key, so disclosing the salt opens the commitment without disclosing the key or enabling decryption of the sealed payload. This permits selective disclosure: a principal may open one record to an auditor without surrendering the ability to open unrelated attestations.
Verification is offline. It requires no call to Rubric and no trust in the issuing node beyond the signature and anchor, both of which are independently checkable.
Implementations MUST reject a record on any field difference, an added or removed field, or an incorrect key. The reference implementation function verifySpendCommitment is adversarially tested against each case.
The commitment above is carried inside the signed leaf message, which is what the Merkle chain actually commits to. The full path from a decision record to the ledger is:
record + payloadKey -> payload_commitment payload_commitment -> leafMessage leafMessage -> leafHash SHA-256(0x00 || RFC8785(leafMessage)) leafHash -> batchRoot SHA-256(0x01 || L || R) batchRoot -> aggregateRoot aggregateRoot -> ledger
The leaf message contains the attestation id, issuing node region, issue time and the payload commitment. It carries no plaintext, so publishing it discloses nothing about the decision. Implementations MUST expose it in proof packages; without it a verifier can confirm that something was anchored but cannot tie the anchor to any record.
A verifying party given the leaf message alone can recompute leafHash and walk the rest of the chain. Given additionally the decision record and its payloadKey, it can recompute the payload commitment and confirm that this exact record — its agent, mandate, intent and inputs — is the one anchored.
Attestations are anchored in two hops: leaf to batch root, then batch root to the aggregate root published on the ledger. The second hop's strength depends on the tree version recorded in the tier-2 opening bundle, and implementations MUST surface it rather than presenting all anchors as equivalent.
Verification responses MUST carry the binding strength. A weak anchor presented without qualification overstates what the ledger record demonstrates. In the reference implementation GET /v1/verify/{id} returns aggregateBinding and aggregateTreeVersion at the top level; GET /v1/proof/{id} returns the same determination as hop2.treeVersion alongside the flush records needed to recompute the root.
The aggregate root published to the ledger is signed by an M-of-N quorum of federation regions, each holding its own ML-DSA-65 key. The reference deployment uses 3-of-5 across five regions. Signing occurs once per anchor rather than once per record; every attestation in the batch inherits federation signing through its Merkle path.
What the quorum signs is a canonicalized object binding the anchor identity, the aggregate root, the batch shape and the time:
attestation_type "threshold-multisig" subject "tier2-aggregate" anchor_id identifier of this anchor aggregate_root the root published to the ledger tier1_count number of tier-1 batches in the anchor total_items number of attestations across those batches anchored_at RFC 3339 UTC quorum { required, total, signer_regions }
A verifier checks each signature against the public key stated alongside it, and counts distinct regions against quorum.required.
Limits of the property. Peers sign the aggregate root; they do not receive the underlying leaves and cannot inspect batch contents. The quorum therefore establishes that a stated root existed at a stated time with M regions witnessing it. It does not establish that the issuing node constructed that root honestly from real submissions. What it prevents is retroactive alteration: once M regions have signed, changing what a batch contained requires M private keys across M jurisdictions rather than one. Implementations MUST NOT describe the quorum as preventing a single node from producing an attestation. Authorship is established by section 4.1, not by this section.
Ledger commitment to the signature set. Signing is deliberately off the anchoring critical path: a node must be able to anchor while peers are unreachable. The quorum for an anchor therefore resolves after that anchor is published, and the signature set cannot appear in its own message.
Each anchor instead carries the signature-set hash of the anchor before it:
prevFederationSigHash SHA3-256 over the canonicalized signature set prevAnchorId identifier of the anchor it refers to
An auditor verifying anchor N obtains the signature set from the opening bundle and checks it against the hash carried in N+1. The reference implementation returns federation.sigHash and a federation.ledgerCommitment block naming the mirror-node query that locates the committing anchor, so the auditor does not have to search the topic; the committing message MUST carry prevFederationSigHash equal to that value and prevAnchorId equal to the anchor under examination.
Once N+1 is on the ledger, the signature set for N cannot be forged retroactively. The most recent anchor is therefore always uncommitted until the next one lands, typically within one flush interval.
Chain start. Forward chaining necessarily begins somewhere: anchors written before the mechanism was deployed carry federation signatures in their opening bundles but were never committed by a successor. In the reference deployment the first committing anchor is HCS sequence 282843. An implementation MUST report uncommitted anchors as such rather than directing a verifier to search for a commitment that does not exist.
Degraded state. If fewer than M signatures are obtained, the anchor proceeds and the opening bundle records federationSigned false; GET /v1/proof/{id} reports it as federation.signed with federation.obtained below federation.required. Anchoring MUST NOT block on peer availability. Verification responses MUST surface the quorum state; an anchor signed by fewer than M regions MUST NOT be presented as federation-signed.
An implementation operating in enforce mode MUST NOT invoke the payment function unless the issuing node has acknowledged A2 with a server-assigned attestation ID. Acknowledgement means an HTTP success response containing that ID; a locally generated identifier does not satisfy this requirement.
Failures that MUST block the payment include transport failure, timeout, authentication failure, and quota exhaustion.
An implementation MAY offer a log mode in which attestation failure is recorded and the payment proceeds. Log mode MUST mark the resulting evidence as unattested and MUST NOT be represented as conformant to this profile.
Asynchronous, queued, or fire-and-forget submission does not satisfy enforce mode. This is the profile's most easily violated requirement: an SDK that returns a client-minted identifier before the server has accepted the record produces evidence that appears complete and is not.
An issuing node that accepts client-signed records MUST reject a submission carrying a signature that does not verify. The reference implementation returns HTTP 422 with code CLIENT_SIGNATURE_INVALID and a reason naming the failure: signature does not verify, unsupported algorithm, or malformed encoding.
An absent signature MUST NOT be rejected. Requiring one would exclude every caller that does not sign, and section 4.1 is optional by design. The rule is narrow: present-and-invalid rejects, present-and-valid is carried into the leaf, absent proceeds unsigned.
The asymmetry is deliberate. An unsigned record honestly evidences what the node received. A record bearing a signature that does not verify evidences nothing while appearing to evidence authorship, and it is the appearance that makes it worse than silence.
Enforcement state MUST be operator-visible. The reference implementation reads its mode per request rather than at startup, so a deployment can revert to recording without rejecting, without redeploying.
Attestations are signed with ML-DSA-65 (FIPS 204), batched into a Merkle forest, and anchored to Hedera Consensus Service. The aggregate root written to the ledger is additionally signed by a federation quorum per section 7.3, and where the submitter signed its own record, the leaf carries the authoring key per section 4.1. The anchor fixes the commitment in consensus time and is retrievable by any party from a public mirror node without Rubric's participation.
Anchoring is asynchronous with respect to the gate. The gate requires durable acceptance; consensus anchoring follows. Implementations requiring the anchor before settlement should poll for status anchored.
Reference records produced by the reference SDK v1.4.0:
An independent implementation, written from this specification with no Rubric code, recomputes that aggregate root from the tier-1 flush records returned by GET /v1/proof/{attestationId} and matches the value on the ledger.
A reference verifier, a sample proof package, and worked output for both a sound and a defective anchor are published at https://rubric-protocol.com/docs/verify — runnable with no account and no API key. The verifier source alone is at https://rubric-protocol.com/docs/verify-apa.py. It reimplements the tier-2 aggregation from this specification, uses no Rubric code, and reads the anchor from Hedera's public mirror node. The only Rubric input is the proof package itself, which the attestation holder supplies.
curl -H "x-api-key: $KEY" \ https://rubric-protocol.com/verify/v1/proof/{attestationId} > proof.json python3 verify-apa.py proof.json
The verifier distinguishes binding strength. A treeVersion 3 anchor verifies end to end and exits 0. A treeVersion 2 anchor reports hop 1 and the ledger anchor as verified, warns that hop 2 is not cryptographically bound, and exits non-zero. Implementations of this profile SHOULD behave the same way: a verifier that reports a weak anchor as fully verified is not conformant.
Retrieval via GET /v1/verify/{attestationId} returns the signature, public key, batch root, commitment, and anchor coordinates. Before flush the record reports signed-pending-flush; after flush, anchored with a sequence number.
Key custody. The payloadKey is the sole means of opening a commitment. Loss renders the record unverifiable though still anchored. Principals intending to demonstrate compliance MUST persist it alongside the record.
Record retention. The node stores no plaintext. A principal who discards the record retains an anchor to evidence they can no longer produce.
Timestamp trust. The timestamp field is client-generated and unverified. The trustworthy time bound is the HCS consensus timestamp, which is an upper bound on when the record existed.
Parameter disclosure. The paramsHash field commits without disclosing, so a party can prove which inputs were used while showing nothing. Where params is included, the parameters themselves are recoverable from the encrypted payload by the holder of the payloadKey and by no one else — not by the node operator, not by a reader of the ledger. Where params is omitted, the inputs can be proven only if the principal retained them separately.
Quorum witnesses a root, not its contents. Federation peers cosign the aggregate root without receiving the leaves beneath it. A principal relying on the quorum should understand it as protection against retroactive alteration, not as independent validation that the batch was assembled honestly. Where that distinction matters, client signing per section 4.1 is the mechanism that addresses it.
Signature presence versus validity. A record carrying an unverifiable signature is more dangerous than an unsigned one, because a reader who sees a public key infers authorship. Implementations MUST NOT store, anchor, or return such a signature as though it were evidence.
Scope. This profile evidences agent payment decisions. It does not authenticate, authorize, or enforce policy. It is complementary to identity and mandate layers, not a substitute for either.
This profile sits beneath agent identity and mandate schemes rather than competing with them. Where a Know Your Agent layer establishes standing and a mandate layer bounds authority, this profile produces the evidentiary artifact a regulator, auditor, or counterparty inspects after the fact.
Comments and implementation reports: contact@rubric-protocol.com