Independent AI agent for EU Deforestation Regulation compliance, not affiliated with the EU. Scope & disclaimer →
eudr.dev

Compliance audit · eudr-a2a

Last audited 2026-05-17 against the primary sources below. Every claim in this document is a code reference (file:line) and a regulation citation.

Sources

Annex II. Fields the DDS carries

Annex II itemRegulation text (short)Engine fieldCode
1Operator's name, address, and EORI (per Reg. 952/2013 Art. 9)1_operatorAnnexII::operator
2HS code, free-text description (incl. trade name, scientific name where applicable), and quantity (kg + supplementary unit where applicable)2_productProductDescriptor
3Country of production + geolocation of every plot3_geolocation (Vec\<Geolocation\>)Geolocation
~~4~~~~Upstream-DDS reference~~ deleted by 2025/2650absentAnnexII — comment block confirms intentional absence
5Verbatim declaration text5_declarationANNEX_II_VERBATIM_DECLARATION; byte-exact, 287 bytes
6Signature block (signed-for-and-on-behalf-of / date / function / signature)6_signatureSignature; ed25519 base64 + DID + pubkey

The engine emits these as JSON keys exactly as written, so an auditor can hold the regulation open next to the JSON and read top-to-bottom.

Articles. What the engine enforces and where

Art. 2(13). Cut-off date

"deforestation-free" means produced on land not subject to deforestation after 31 December 2020

WhereWhat
CUT_OFF_DATE"2020-12-31" constant
runner.rsPassed verbatim to emem's /v1/eudr_dds request as cut_off_date
Report"CUT-OFF 31 Dec 2020" tag in the tags strip; cut-off vertical line on field-history timeline

Art. 2(28). Geolocation precision and polygon rule

"geolocation" means the geographical location of a plot of land described by means of latitude and longitude coordinates corresponding to at least one latitude and one longitude point and using at least six decimal digits [..] for plots of more than 4 hectares used for the production of the relevant commodities other than cattle, polygons [..]. For cattle, the geolocation refers to all establishments [..]

RuleCodeStatus
≥ 6 decimal digits required on every coordinatevalidate_plot_geometryEnforced. Server returns 400 with the exact precision reported.
Cattle requires polygon regardless of areaCommodity::polygon_always_required consulted by validate_plot_geometryEnforced. Cattle + Point → 400.
Non-cattle plots > 4 ha must be polygonsring_area_ha computed; Point geometries are accepted under that areaPolygon shape is the operator's responsibility above 4 ha; the validator records area_ha so the DDS document carries it.

Test: art_2_28_rejects_under_6_decimal_precision. Test: art_2_28_cattle_rejects_point_geometry. Sample data: every samples/scenarios/.json polygon now meets the 6-digit minimum.

Art. 3. Prohibition on placement

Relevant commodities and relevant products shall not be placed on the market [..] unless [..] (a) [..] deforestation-free; (b) [..] in accordance with the relevant legislation of the country of production; (c) [a DDS] has been made available [..].

WhereWhat
runner.rsWhen verdict is NonNegligible, the DDS document carries a non-attestation declaration instead of the Annex II point-5 wording, and the schema id is suffixed .assessment so downstream verifiers can refuse to forward it as a DDS.
ReportWhen verdict is non-negligible the printable report renders "Don't ship yet" + an explicit Art. 4(1) sentence.

This avoids the false-attestation trap: a signed document with the verbatim "no or only a negligible risk was found" wording over a non-negligible verdict would be a literal false statement.

Test: composite_risk tests cover every tier × fail-fraction combination including the high-risk floor.

Art. 4(1). Operator must not place product

The runner emits PipelineEvent::Blocked on non-negligible verdicts. The CLI and server present this state and refuse to mark a task paid on the assessment-only schema.

Art. 4(2). DDS reference with the customs declaration

The scenario carries an optional production.customs_declaration_ref (MRN or local reference). The report's Customs & Shipping (3b) section surfaces this field; the engine does not pre-validate the MRN format (operators in different member states use different formats).

Art. 4(3). Five-year retention

Operators shall keep [..] the due diligence statements they have submitted [..] for a period of five years from the date of submission.

WhereWhat
RECORD_RETENTION_YEARS = 5Constant surfaced in the report footer and Art. 12 panel
EUDR_TASK_RETENTION_DAYS env (default 7)In-process working-memory window. The 5-year archive is the operator's responsibility.
DELETE /a2a/tasks/:idClears scenario + DDS payload but preserves the receipt CID + audit aggregate (Art. 17(3)(b) GDPR).

Art. 8. Three steps of due diligence

StepEngine stage
Art. 8(2)(a) — information collection (Art. 9)geo + forest stages
Art. 8(2)(b) — risk assessment (Art. 10)legality + risk stages
Art. 8(2)(c) — risk mitigation (Art. 11)The engine signals required mitigation in the report's Article 11 panel; the act of mitigating belongs to the operator.

Art. 9(1). What the operator collects

Coverage matrix is surfaced in the report as the "Article 9(1) · information collected" panel.

Sub-articleField on ScenarioEngine behaviour
(a) description, type, scientific nameproduct.description, product.trade_name, product.scientific_nameDescription and HS required; trade name + scientific name optional but recommended
(b) quantity (kg + supplementary unit where applicable)product.quantity_kg, product.supplementary_unit, product.supplementary_quantityQuantity > 0 enforced
(c) country (and sub-region where relevant) of productionproduction.country, production.country_subregionCountry required
(d) geolocation of every plot + date or interval of productionproduction.plot_geojson + additional_plots; production.harvest_windowRequired; ≥ 6 decimals enforced
(e) supplier name, postal address, emailsupplier.name, supplier.address, supplier.emailName + ID required; address + email optional but the report flags absence in the Art. 9 coverage matrix
(f) downstream recipient details (when applicable)Out of scope (operator is the recipient on first placement)Coverage matrix marks (f) with a contextual note
(g) deforestation-free evidence (conclusive + verifiable)source_receipts.emem.per_cell[].fact_cidsEvery per-cell verdict carries fact CIDs; signed receipt envelope verifies offline
(h) lawful production evidencelegality.findingsBaseline runs country-tier + supplier-id checks; land tenure and harvest permits remain in scope for a partner provider behind LegalityProvider

Art. 9(1). Five-year retention

Same five-year horizon as Art. 4(3). The receipt CID stays as the audit anchor even after operator-side erasure (Art. 17(3)(b) GDPR).

Art. 10. Risk assessment

Reported in the report's "Article 10 · risk assessment" panel. Criteria (a)–(k) listed with what the engine assessed and what it did not. The composite-risk function is exposed and tested:

composite = clamp(fail_fraction × 0.85 × tier_mult, 0, 1)

Test: 8 cases in composite_risk tests.

Art. 10(2)(j). Risk of mixing / substitution

Operator-declared third-party certifications (RSPO, FSC, RA, Fairtrade, ISCC_EU, PEFC, …) are recorded in scenario.certifications and rendered in the report. They are surfaced in the Art. 10 row as mitigation evidence, not a substitute for due diligence.

Art. 11. Risk mitigation

The report's Article 11 panel is verdict-aware:

Art. 11 measures listed, plus a note that the report must NOT be submitted as a negligible-risk attestation.

Art. 12. DD system + Art. 4(2) submission

The report's Article 12 panel records the engine identity (eudr-a2a, vortx-ai/eudr), notes annual reporting under Art. 12(3), and flags submission status:

TRACES SOAP submission is on the roadmap; the engine output is otherwise submission-ready.

Art. 13. Simplified due diligence

simplified_dd_applied is set true only when the production country is on the Implementing Reg. 2025/1093 low-risk list. The flag is recorded in conclusion.simplified_dd_applied and surfaced as a tag on the report.

Art. 29. Country benchmarking

The country tier table is data/country_benchmarks_2025_1093.json, loaded at startup. High-risk (4): BY, MM, KP, RU. Conservative low-risk list (~140). Anything unlisted resolves to STANDARD.

Art. 38 (replaced by 2025/2650). Application dates

GroupApplication dateConstant
Default (most operators + medium-sized undertakings)30 December 2026APPLICATION_DATE_DEFAULT
Natural persons + micro / small operators established by 31 Dec 2024 (FLEGT-Annex products excluded)30 June 2027APPLICATION_DATE_DEFERRED_MICRO_SMALL_NATURAL

Surfaced in the agent card's /spec page on the public console.

Signed proof

PropertyValue
Algorithmed25519 over canonical JSON bytes
CIDCIDv1, raw codec (0x55), BLAKE3-256 multihash
CanonicalisationHand-implemented JCS-style: sorted keys, no insignificant whitespace
Round-trip safetycanonical_b64 field carries the exact bytes that were signed and hashed, so JSON re-serialisation cannot break verification
Verifiereudr verify <path> (offline); POST /api/verify_receipt (online via emem)

Tamper detection test: tamper_detected.

TRACES NT submission output

Three formats, one CLI:

eudr compile samples/scenarios/civ-cocoa.json --out civ.json

# Engine-annotated XML (verbose; namespace urn:vortx-ai:eudr-a2a)
eudr export-traces civ.json --format engine --out civ.engine.xml

# EU Commission TRACES NT v2 shape. The wire format the Information System accepts
eudr export-traces civ.json --format official --activity IMPORT --out civ.official.xml

# Full SOAP envelope with WS-Security UsernameToken-Digest, ready to POST
EUDR_TRACES_USERNAME=service@deployer.eu EUDR_TRACES_PASSWORD=… \
eudr export-traces civ.json --format soap --activity IMPORT --out civ.soap.xml

civ.official.xml namespace: http://ec.europa.eu/tracesnt/certificate/eudr/submission/v2. Root element: <eudr:SubmitStatementRequest>. Field names match the EU Commission's v2 API spec (operatorType, statement.activityType, statement.countryOfActivity, statement.commodities[].hsHeading, statement.commodities[].producers[].geometryGeojson base64-encoded, operator.operatorAddress.fullAddress, …).

civ.soap.xml adds a SOAP 1.1 envelope with the WS-Security header: UsernameToken with PasswordDigest = base64(SHA1(nonce ‖ created ‖ password)), fresh 16-byte CSPRNG nonce, RFC-3339 UTC Created timestamp. TRACES NT's EUDRSubmissionServiceV2 endpoint accepts this verbatim.

civ.engine.xml is what we already emitted. Useful for archival and internal audit because every <EngineConclusion> and source-cited element is namespaced separately from the official submission shape.

Continuous monitoring (Art. 8(2))

# A2A SendMessage on the new skill (advertised in the agent card):
curl -X POST https://eudr.dev/a2a/rpc \
 -H 'content-type: application/json' \
 -d '{"jsonrpc":"2.0","id":"r","method":"SendMessage",
 "params":{"task_type":"eudr.monitor.subscribe",
 "input":{"dds_reference":"eudr-a2a:engine:CI:…","cadence":"monthly"}}}'

The agent re-runs the deforestation check on the cadence, when a new Hansen loss year appears post-cut-off for any sampled cell, the subscription emits a SignedReceipt-shaped event citing Art. 8(2). The first re-check after each annual Hansen update is free; later checks are priced per call.

civ.xml is a namespaced XML carrying every Annex II point that TRACES NT requires:

<DueDiligenceStatement
 xmlns="urn:vortx-ai:eudr-a2a:dds:v2025_2650"
 schema="eu.eudr.dds.v2025_2650"
 submission_status="pre_submission">
 <Header>..</Header>
 <Operator>..<EORI>..</EORI>..</Operator> <!-- point 1 -->
 <Product><HsCode>..</HsCode><Cn8>..</Cn8>..</Product> <!-- point 2 -->
 <Geolocation><Plot index="0">..</Plot>..</Geolocation> <!-- point 3 -->
 <!-- point 4 deleted by Reg. (EU) 2025/2650 -->
 <Declaration verbatim="true">..</Declaration> <!-- point 5 -->
 <Signature>
 <Name>..</Name><Place>..</Place><Date>..</Date>
 <Function>..</Function>
 <CryptographicProof>
 <Algorithm>ed25519</Algorithm>
 <SignatureBase64>..</SignatureBase64>
 <PublicKeyBase32>..</PublicKeyBase32>
 <SignerDid>..</SignerDid>
 </CryptographicProof>
 </Signature> <!-- point 6 -->
 <EngineConclusion>..</EngineConclusion>
</DueDiligenceStatement>

submission_status is pre_submission for filable, not_filable when the schema is .assessment, draft when the document is unsigned. A production TRACES adapter replaces the engine namespace with urn:eu:eudr:dds:v1, wraps the document in the official SOAP envelope, and POSTs to https://eudr.webcloud.ec.europa.eu/tracesnt/. The <EngineConclusion> block is dropped on submission (it is the engine's audit annex, not part of Annex II).

What the engine does NOT do

Documented openly so an auditor can see the boundary:

  1. Art. 9(1)(b). Lawful production beyond country-tier. emem.dev's

own response carries a legality_disclaimer saying production-law checks are structurally outside Earth-observation scope. The baseline legality provider here runs country-tier + supplier-id checks. Land-tenure deeds, harvest permits, and FPIC under Art. 10(2)(c)–(e) require a partner legality provider plugged behind the LegalityProvider trait. The report's Article 10 panel marks (c)–(e), (j) and (k) as "Not assessed by this engine."

  1. TRACES NT submission. The engine produces a submission-ready

document. The SOAP envelope and CIRCABC credential flow are on the roadmap.

  1. HSM-backed signing in production. The bundled dev key is

deterministic and useful only for development. The server refuses to boot with the dev key over HTTPS. Production deployments must set EUDR_AGENT_PRIVKEY_B64 to a real 32-byte secret (ideally sourced from an HSM via KMS).

  1. Replacement for the operator's compliance programme. The

regulation places the obligation on the operator. The engine is a processor that runs steps required by Art. 8(2) and emits a signed record. The operator retains responsibility for verifying inputs and for the final submission decision.

Audit log

DateAuditorFindingFix
2026-05-17self-auditArt. 2(28) ≥ 6-decimal precision not enforced (sample data had 4 decimals; engine accepted)Added validate_plot_geometry rejecting < 6 decimals; regenerated every sample to 6-decimal coords; geom test threshold raised.
2026-05-17self-auditArt. 2(28) cattle-polygon rule not enforcedvalidate_plot_geometry rejects Point + cattle.
2026-05-17self-auditNon-negligible verdict still produced a signed Annex II document carrying the verbatim attestation ("no or only a negligible risk was found")Schema id now suffixed .assessment on non-negligible verdicts; declaration text replaced with a non-attestation explanation. The receipt schema matches so downstream verifiers can route or reject without parsing the payload.
2026-05-17self-auditPolygon-area Hansen post-cut-off loss could be missed by the 16-cell eudr_dds sampler and silently produce a "low risk" verdictReport's live land-cover panel now compares the polygon-area Hansen scan to the per-cell sampler and emits an amber warning when they disagree, citing Art. 8(2).
2026-05-17self-auditThreshold-based verdict (composite < 0.20 → negligible) ignored confirmed per-cell post-cut-off loss. Art. 2(13) is a binary condition.Runner now flips to NonNegligible when any per-cell entry has hansen_lossyear >= 21 (or >= 2021 full-year) OR a fail-class label. Threshold is now a secondary gate.
2026-05-17self-auditNo TRACES NT submission output. The PDF / JSON cannot be filed; the EU Information System accepts SOAP/XML only.Added eudr-core::traces::to_traces_xml + eudr export-traces CLI. Output is a namespaced XML carrying every Annex II field plus <EngineConclusion> + cryptographic proof. A TRACES adapter swaps the namespace for the official one and wraps in the SOAP envelope.
2026-05-17self-auditAnnex II point 6 missed the Place of signing. Regulation lists "Place" in the signature block.New Operator.signing_place (optional) + Signature.place. Defaults to country of establishment when absent. Rendered in the PDF signature block AND in the TRACES XML.
2026-05-17self-auditEORI format not validated. Any string was accepted.New is_valid_eori: ISO-alpha-2 + 1.15 alphanumeric (Reg. 952/2013 Art. 9). Server returns 400 with the exact failure mode.
2026-05-17self-auditPolygon GIS sanity (self-intersection, degenerate) not checked. A bow-tie polygon would pass shape + precision checks and produce an ambiguous verdict.check_polygon_simple runs OGC-Simple-Features pair-wise edge crossing + degeneracy check on every plot.
2026-05-17self-auditCN8 (8-digit Combined Nomenclature) not surfaced. EU customs declaration + TRACES require CN8; engine accepted HS6.TRACES XML carries both <HsCode> (operator-supplied) and <Cn8> (derived via zero-pad from HS≥4). Operator-supplied CN8 round-trips unchanged.
2026-05-17self-auditReport didn't disclose it's the audit record, not the legal submission.Cover page now has a .status-strip panel: submission status (filable vs assessment vs draft), document role (5-year audit record per Art. 4(3)/9(1)/12(5)), and the verification path (eudr verify, fact-CID dereference URL).
2026-05-17self-auditReport carried only the per-cell verdict matrix and a single satellite chip. No NDVI scalar with capture date, no SAR, no crop-calendar cross-check.Runner now does a second emem pass after eudr_dds: pulls indices.ndvi + sentinel1_raw across the polygon bbox. Each fact carries its Sentinel-2 / Sentinel-1 scene id + captured_at. Surfaced in a new "Sentinel-1 SAR + Sentinel-2 NDVI · single-snapshot per cell" panel with per-cell table, statistics, and the regulation-cited interpretation bands. Disclosed honestly: emem returns single-snapshot, not time-series.
2026-05-17self-auditNo reference cross-check for the operator-declared harvest window.New data/crop_calendars.json table sourced from FAO GIEWS, USDA FAS, ICCO, RSPO. New eudr-core::crop_calendar module performs cross-check per commodity × country and returns in_main_season / in_secondary_season / off_season / no_reference. Surfaced as a 12-month grid in the report comparing the declared window to the reference calendar. Five new tests covering Côte d'Ivoire cocoa, Brazil soy (main + off-season), and the no-reference fallback.

Test coverage

cargo test --workspace. 58 tests passing across the workspace.

Crate / fileTests
eudr-core::commodityfrom_hs18
eudr-core::countryBenchmarkTable::tier (loads shipped JSON)6
eudr-core::eu — EU_MEMBER_STATES sanity5
eudr-pipeline::runnercomposite_risk8
eudr-pipeline::geominspect (incl. civ-cocoa sample)4
eudr-receipts — sign / verify / tamper6
eudr-a2a::skills — skill catalog shape and prices5
eudr-server::routes::validation_tests — Art. 2(28) cases6

Verbatim declaration. Byte-exact check

The Annex II point 5 declaration text is mandated wording. The string in ANNEX_II_VERBATIM_DECLARATION is 287 bytes long and round-trips through the JSON serialiser unchanged:

By submitting this due diligence statement the operator confirms that due
diligence in accordance with Regulation (EU) 2023/1115 was carried out and
that no or only a negligible risk was found that the relevant products do
not comply with Article 3, point (a) or (b), of that Regulation.

This is the consolidated text from CELEX:02023R1115-20241226 Annex II, unchanged by Reg. 2025/2650.

Re-running this audit

cargo test --workspace
cargo run --bin eudr -- compile samples/scenarios/civ-cocoa.json --out /tmp/civ.json
cargo run --bin eudr -- verify /tmp/civ.json
python3 -c "
import json; d=json.load(open('/tmp/civ.json'))['payload']
print('Annex II keys:', sorted(k for k in d if k[:1].isdigit()))
print('Point 4 absent:', '4_' not in str(list(d.keys())))
for g in d['3_geolocation']:
 ring = g['plot_geojson']['coordinates'][0]
 md = min(min(len(repr(c).split('.')[-1]) for c in pt) for pt in ring)
 print(f'plot {g[\"cell64\"]}: precision={md} digits. Art. 2(28):', 'PASS' if md >= 6 else 'FAIL')
print('Verdict:', d['conclusion']['verdict'], '· schema:', d['schema'])
"

AI Act and product-safety posture (verified 2026-06-04)

Because this engine serves other AI agents (A2A v1.0 + MCP), not only humans, its own AI-regulation posture is part of the compliance surface:

rulestatuswhere enforced
AI Act (EU) 2024/1689 Art. 50(1) — disclose AI interaction (applies 2 Aug 2026)Implemented early. Human surfaces brand the service as an AI agent (obvious-from-context limb); the agent card carries a machine-readable ai_system_disclosure block for agent callers.crates/eudr-a2a/src/card.rs (AiSystemDisclosure); test guard agent_card_carries_ai_act_disclosure
AI Act role classificationDownstream deployer; not a GPAI model provider; not Annex III high-risk. No in-force obligation binds the service today.REGULATORY_CROSSWALK.md §Adjacent EU rules
GPSR (EU) 2023/988Out of scope — consumer-product regulation; this is a B2B operator/trader service with no consumer nexus.legal/terms.md §2 (B2B scope)
MDR 2017/745 Annex I ("GSPR")Out of scope — medical devices only. Listed to disambiguate the acronym.

Human oversight invariant: a non-negligible or indeterminate verdict is never auto-filed; TRACES submission is the operator's regulatory act under their own EU Login identity.