Issuer Voucher Contract

Privacy-Preserving Lending Voucher

Everything a lender's engineering team needs to wire one signed, time-bounded lending voucher into their underwriting flow. Plaintext JSON for the bound index fields, opaque ciphertext for the encrypted financial claim, persisted to lending_credentials and reflected on the /lending and /agent-identity explainers within one render.

POST /api/lending/credential/issue

Plaintext JSON for the bound voucher index fields, opaque ciphertext for the encrypted financial history the integrator produced on-device. One voucher per signature; replay within the validity window re-issues the same voucher deterministically.

Method
POST
URL
https://automa-labs-ai.polsia.app/api/lending/credential/issue
Base
https://automa-labs-ai.polsia.app
Content-Type
application/json
Schema
lending-cred-issue-1
Issuer
automaos.lending
Discovery
GET https://automa-labs-ai.polsia.app/api/lending/credential/issue

Time-bound canonicalization

The issuer signs a canonical UTF-8 string built from a structured object whose keys are sorted alphabetically and rendered with no whitespace. The signed bytes cover voucher_id, user_pubkey, claim_id, ciphertext_hash, the issuer name, issued_at, valid_until, and validity_window_seconds. The signed voucher_id — not the encrypted financial claim — is the audit point: a regulator confirms issuance without ever holding the borrower's plaintext history.

Validity bounds

The time-bounded voucher lifetime is clamped to 60…86400s. The same shape also accepts an absolute ISO-8601 valid_until; the two are mutually exclusive.

validity_seconds ∈ [60, 86400] valid_until: ISO-8601 (clamped to issued_at + 24h)

Canonicalization rule

rule
JSON.stringify(obj) with keys sorted alphabetically and no whitespace.

Deterministic voucher_id

voucher_id = "vch_<sha256[:32]>" over (user_pubkey + ":" + claim_id + ":" + ciphertext_hash + ":" + floor(issued_at_ms / (validity_seconds * 1000)) + ":" + issuer). Replay short-circuits on the same (user_pubkey, claim_id, ciphertext_hash, replay_window) tuple — the issuer returns the existing voucher (same signature, same payload_string, replay: true) instead of writing a new row.

Issuer identity

The signing key is server-owned. When LENDING_ISSUER_PRIVATE_KEY is unset the endpoint still works but issues vouchers with an ephemeral key (mock_key: true); those vouchers will not verify across restarts. The fingerprint below is the 16-character SPKI body prefix a visitor can compare against the issuer detail on /lending/applications/<voucher_id> to confirm the issuance event and the lookup describe the same signing identity.

fingerprint issuer MCowBQYDK2VwAyEA…
mock_key — LENDING_ISSUER_PRIVATE_KEY is not set on the server. The full PEM below was generated at boot and is unstable across restart; voucher signatures will not verify once the process is replaced.
pem — issuer public key (SPKI Ed25519)
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA1S5zx5DeS9xUSSBnME3/cxPFhCbPMgpMc5WFrXcaWQ4=
-----END PUBLIC KEY-----

Required fields

All fields are required, except where noted. Any missing or invalid field produces a 400 with the offending fields listed in errors.

Field Type Description
user_pubkey string Borrower public key identifier — the project's chosen "deterministic per-user ID" field. Used as the schema-level partition for the voucher and replay lookup.
claim_id string Borrower-supplied claim identifier (e.g. "clm_01J9Q").
ciphertext string Opaque encrypted financial-history bytes. The server treats this as opaque and never decrypts; it is hashed and persisted verbatim for audit.
validity_seconds integer Time-bounded voucher lifetime in seconds (60…86400). Mutually exclusive with valid_until.
valid_until string Absolute expiry timestamp. Server clamps to issued_at + 24h.

JSON body the integrator posts

The values below come straight from the live contract — what the integrator sends and what the server signs.

json
{
  "user_pubkey": "pk_loan_4f7e2c1a",
  "claim_id": "clm_01J9Q",
  "ciphertext": "0x9f3a8c1d_demo_ciphertext_octets",
  "validity_seconds": 3600
}

curl invocation

One command — sends the example envelope to the live issuer. The host is whatever the request landed on; the path is the same everywhere.

bash — curl
curl -X POST https://automa-labs-ai.polsia.app/api/lending/credential/issue \
  -H "Content-Type: application/json" \
  -d '{
  "user_pubkey": "pk_loan_4f7e2c1a",
  "claim_id": "clm_01J9Q",
  "ciphertext": "0x9f3a8c1d_demo_ciphertext_octets",
  "validity_seconds": 3600
}'
The endpoint is public — no Bearer header. Replace user_pubkey and claim_id with the per-borrower values your underwriting service generated; ciphertext is the opaque encrypted financial-history bytes the browser produced on-device — the server treats it as opaque, hashes it, persists it for audit, and never decrypts it. validity_seconds and valid_until are mutually exclusive: supply exactly one, in [60, 86400]… seconds or an ISO-8601 timestamp clamped to issued_at + 24h.

What the server can give back

Every terminal status the integrator needs to handle.

Status Body When
200 {"ok":"true","voucher_id":"string (vch_<sha256[:32]>)","user_pubkey":"string","claim_id":"string","issuer":"string","issued_at":"ISO-8601 string","valid_until":"ISO-8601 string","signature":"string (128-hex Ed25519)","payload_string":"string (canonical UTF-8 bytes the issuer signed)","ciphertext_hash":"string (64-hex sha256 over the opaque ciphertext)","determinism":{"replay_window_seconds":"integer"},"issuer_pub_key_pem":"string (PEM SPKI Ed25519)","mock_key":"boolean","replay":"boolean — true if this is a same-window replay (no DB write)"} Voucher issued or replayed: signed canonical payload persisted to lending_credentials; voucher_id, signature, payload_string, and issuer_pub_key_pem returned. replay: true indicates a same-window replay (no DB write).
400 {"ok":"false","errors":["string"]} Validation error — one or more required fields are missing, wrong type, or validity_seconds / valid_until was supplied incorrectly.

The signed voucher, not the encrypted claim, is the audit point.

A borrower produces an encrypted financial-history claim on-device, the issuer signs a deterministic time-bounded voucher over the claim hash, and a lender settles against the voucher without ever receiving plaintext financials — the /lending flow end-to-end. Apply at /lending/apply, track any vch_<hex> voucher at /lending/applications/<voucher_id>, and drop the curl above into your underwriting pipeline to issue one signer-on-behalf flow. Two distinct borrowers always produce two distinct voucher IDs even when the ciphertext itself stays opaque: the vch_<sha256[:32]> namespace is portable, the replay window is floored by validity_seconds, and the signing identity is server-owned — the issuance event is the audit point, never the encrypted financial history.

What happens to a successful voucher

Every accepted voucher is written to the lending_credentials table, keyed UNIQUE on voucher_id, and indexed on user_pubkey and valid_until. The same contract — including the example envelope and the response shapes — is published as plain JSON at https://automa-labs-ai.polsia.app/api/lending/credential/issue, so an integrator can pull the spec straight off the live endpoint rather than caching this page.